From 354a42a1a4b4bc301442dc77275be9bde05aef3f Mon Sep 17 00:00:00 2001 From: Asa Rentschler Date: Wed, 4 Sep 2024 10:52:15 -0400 Subject: [PATCH 01/10] Initial rework. System works but is buggy and badly written --- .gitignore | 2 + README.md | 2 +- {assets => misc}/artwork/icon.drawio | 0 {assets => misc}/artwork/icon.drawio.png | Bin setup-env.sh => misc/scripts/setup-env.sh | 0 vtmp.vim => misc/vim/vtmp.vim | 0 pyproject.toml | 23 + src/velocity/__init__.py | 5 + velocity => src/velocity/__main__.py | 105 +-- lib/backends.py => src/velocity/_backends.py | 24 +- lib/build.py => src/velocity/_build.py | 101 ++- src/velocity/_config.py | 257 ++++++ .../velocity/_exceptions.py | 18 + src/velocity/_graph.py | 752 ++++++++++++++++++ lib/graph.py => src/velocity/_graph_old.py | 2 +- lib/print.py => src/velocity/_print.py | 0 {lib => tests}/__init__.py | 0 tests/test__config.py | 59 ++ tests/test__graph.py | 197 +++++ 19 files changed, 1402 insertions(+), 145 deletions(-) rename {assets => misc}/artwork/icon.drawio (100%) rename {assets => misc}/artwork/icon.drawio.png (100%) rename setup-env.sh => misc/scripts/setup-env.sh (100%) rename vtmp.vim => misc/vim/vtmp.vim (100%) create mode 100644 pyproject.toml create mode 100644 src/velocity/__init__.py rename velocity => src/velocity/__main__.py (75%) mode change 100755 => 100644 rename lib/backends.py => src/velocity/_backends.py (94%) rename lib/build.py => src/velocity/_build.py (66%) create mode 100644 src/velocity/_config.py rename lib/exceptions.py => src/velocity/_exceptions.py (77%) create mode 100644 src/velocity/_graph.py rename lib/graph.py => src/velocity/_graph_old.py (99%) rename lib/print.py => src/velocity/_print.py (100%) rename {lib => tests}/__init__.py (100%) create mode 100644 tests/test__config.py create mode 100644 tests/test__graph.py diff --git a/.gitignore b/.gitignore index aeb354d..4201467 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ ############################################################ tmp *__pycache__ +dist +*\.egg-info diff --git a/README.md b/README.md index 89d38dc..bf495b8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

VELOCITY

## Description -Container build system. +A container build manager. ## Documentation Run: diff --git a/assets/artwork/icon.drawio b/misc/artwork/icon.drawio similarity index 100% rename from assets/artwork/icon.drawio rename to misc/artwork/icon.drawio diff --git a/assets/artwork/icon.drawio.png b/misc/artwork/icon.drawio.png similarity index 100% rename from assets/artwork/icon.drawio.png rename to misc/artwork/icon.drawio.png diff --git a/setup-env.sh b/misc/scripts/setup-env.sh similarity index 100% rename from setup-env.sh rename to misc/scripts/setup-env.sh diff --git a/vtmp.vim b/misc/vim/vtmp.vim similarity index 100% rename from vtmp.vim rename to misc/vim/vtmp.vim diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..491927a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[build-system] +requires = [ "setuptools>=61.0" ] +build-backend = "setuptools.build_meta" + +[project] +name = "velocity" +dynamic = [ "version" ] +authors = [ + { name="Asa"} +] +description = "A container build manager" +readme = "README.md" +dependencies = [ + "pyyaml", + "networkx", + "colorama", + "editor", + "loguru" +] + +[project.urls] +Homepage = "https://github.com/olcf/velocity" +Issues = "https://github.com/olcf/velocity/issues" diff --git a/src/velocity/__init__.py b/src/velocity/__init__.py new file mode 100644 index 0000000..6402a0d --- /dev/null +++ b/src/velocity/__init__.py @@ -0,0 +1,5 @@ +from ._config import config + +__version__ = "0.0.0" + +config.set("velocity:version", __version__) diff --git a/velocity b/src/velocity/__main__.py old mode 100755 new mode 100644 similarity index 75% rename from velocity rename to src/velocity/__main__.py index a8ec53a..7fbdd35 --- a/velocity +++ b/src/velocity/__main__.py @@ -1,16 +1,14 @@ -#!/usr/bin/env python3 - import argparse import re import os import editor from pathlib import Path from colorama import Fore, Style -from lib.graph import Node, ImageGraph, Target, DepOp -from lib.build import Builder -from lib.print import p1print, sp1print, TextBlock, wprint, print_text_blocks +from ._graph import ImageRepo, Target, DepOp +from ._build import Builder +from ._print import p1print, sp1print, TextBlock, wprint, print_text_blocks +from ._config import config -VERSION = 'beta.0.0' ############################################################ # Main @@ -20,49 +18,14 @@ ############################################################ # Load Config ############################################################ - SETTINGS = dict() - - if os.getenv('VELOCITY_IMAGE_DIR') is not None: - SETTINGS['VELOCITY_IMAGE_DIR'] = os.getenv('VELOCITY_IMAGE_DIR') - else: - wprint([ - TextBlock("VELOCITY_IMAGE_DIR not set!") - ]) - exit(1) - if os.getenv('VELOCITY_SYSTEM') is not None: - SETTINGS['VELOCITY_SYSTEM'] = os.getenv('VELOCITY_SYSTEM') - else: - wprint([ - TextBlock("VELOCITY_SYSTEM not set!") - ]) - exit(1) - + config.set("velocity:system", os.getenv('VELOCITY_SYSTEM')) if os.getenv('VELOCITY_BACKEND') is not None: - SETTINGS['VELOCITY_BACKEND'] = os.getenv('VELOCITY_BACKEND') - else: - wprint([ - TextBlock("VELOCITY_BACKEND not set!") - ]) - exit(1) - + config.set("velocity:backend", os.getenv('VELOCITY_BACKEND')) if os.getenv('VELOCITY_DISTRO') is not None: - SETTINGS['VELOCITY_DISTRO'] = os.getenv('VELOCITY_DISTRO') - else: - wprint([ - TextBlock("VELOCITY_DISTRO not set!") - ]) - exit(1) - + config.set("velocity:distro", os.getenv('VELOCITY_DISTRO')) if os.getenv('VELOCITY_BUILD_DIR') is not None: - SETTINGS['VELOCITY_BUILD_DIR'] = os.getenv('VELOCITY_BUILD_DIR') - else: - wprint([ - TextBlock("VELOCITY_BUILD_DIR not set!") - ]) - exit(1) - - SETTINGS['VERSION'] = VERSION + config.set("velocity:build_dir", os.getenv('VELOCITY_BUILD_DIR')) ############################################################ # Parse Args @@ -72,7 +35,7 @@ description='Build tool for OLCF containers', epilog='See (https://gitlab.ccs.ornl.gov/saue-software/velocity)') parser.add_argument('-v', '--version', action='version', - version=f"%(prog)s {SETTINGS['VERSION']}", help="program version") + version=f"%(prog)s {config.get('velocity:version')}", help="program version") parser.add_argument('-b', '--backend', action='store') parser.add_argument('-s', '--system', action='store') parser.add_argument('-d', '--distro', action='store') @@ -112,21 +75,21 @@ ############################################################ # apply user run time arguments over settings ############################################################ - SETTINGS['VELOCITY_SYSTEM'] = args.system if args.system is not None else SETTINGS['VELOCITY_SYSTEM'] - SETTINGS['VELOCITY_BACKEND'] = args.backend if args.backend is not None else SETTINGS['VELOCITY_BACKEND'] - SETTINGS['VELOCITY_DISTRO'] = args.distro if args.distro is not None else SETTINGS['VELOCITY_DISTRO'] + if args.system is not None: + config.set("velocity:system", args.system) + if args.backend is not None: + config.set("velocity:backend", args.backend) + if args.distro is not None: + config.set("velocity:distro", args.distro) ############################################################ # Load images ############################################################ - imageGraph = ImageGraph( - str(Path(SETTINGS['VELOCITY_IMAGE_DIR']).absolute()), - SETTINGS['VELOCITY_BACKEND'], - SETTINGS['VELOCITY_SYSTEM'], - SETTINGS['VELOCITY_DISTRO'] - ) + imageRepo = ImageRepo() + imageRepo.import_from_dir("/home/xjv/PycharmProjects/velocity-images") # print backend, system, distro + """ p1print([ TextBlock(f"System: "), TextBlock(f"{SETTINGS['VELOCITY_SYSTEM']}", fore=Fore.MAGENTA, style=Style.BRIGHT) @@ -140,30 +103,16 @@ TextBlock(f"{SETTINGS['VELOCITY_DISTRO']}", fore=Fore.MAGENTA, style=Style.BRIGHT) ]) print() # add newline + """ ############################################################ # Handle User Commands ############################################################ if args.subcommand == 'build': # parse targets - targets = [] - for target in args.targets: - result = re.search(r'^(.*)@(.*)([%^_=])(.*)$', target) - if result is not None: - if result[3] == '=': - targets.append(Target(Node(result[1], result[4]), DepOp.EQ)) - elif result[3] == '^': - targets.append(Target(Node(result[1], result[4]), DepOp.GE)) - elif result[3] == '_': - targets.append(Target(Node(result[1], result[4]), DepOp.LE)) - elif result[3] == '%': - targets.append(Target(Node(result[1], result[2]), DepOp.GE)) - targets.append(Target(Node(result[1], result[4]), DepOp.LE)) - else: - targets.append(Target(Node(target, ''), DepOp.UN)) # get recipe - recipe = imageGraph.create_build_recipe(targets) + recipe = imageRepo.create_build_recipe(args.targets) # print build specs p1print([ @@ -171,20 +120,16 @@ ]) for r in recipe: sp1print([ - TextBlock(f"{r.name}@={r.tag}", fore=Fore.MAGENTA, style=Style.BRIGHT) + TextBlock(f"{r.name}@{r.version}", fore=Fore.MAGENTA, style=Style.BRIGHT) ]) print() # add newline # prep builder builder = Builder( recipe, - SETTINGS['VELOCITY_BACKEND'], - SETTINGS['VELOCITY_SYSTEM'], - SETTINGS['VELOCITY_DISTRO'], build_name=args.name, dry_run=args.dry_run, leave_tags=args.leave_tags, - build_dir=SETTINGS['VELOCITY_BUILD_DIR'], verbose=args.verbose ) @@ -195,7 +140,7 @@ # group and order grouped = dict() - for node in imageGraph.nodes: + for node in imageRepo.images: if node.name not in grouped: grouped[node.name] = list() grouped[node.name].append(node) @@ -211,7 +156,7 @@ deps.sort() for t in deps: sp1print([ - TextBlock(t.tag, fore=Fore.YELLOW, style=Style.BRIGHT) + TextBlock(t.version, fore=Fore.YELLOW, style=Style.BRIGHT) ]) print() # add newline @@ -313,7 +258,7 @@ def spec_print(seed: str, indent: int, fdt: dict, rs: tuple[Node]): raise NotADirectoryError(tag) else: raise NotADirectoryError(name) - + elif args.subcommand == 'create': image_dir = Path(SETTINGS['VELOCITY_IMAGE_DIR']).joinpath(args.name, args.version) if image_dir.is_dir(): @@ -337,4 +282,4 @@ def spec_print(seed: str, indent: int, fdt: dict, rs: tuple[Node]): else: parser.print_help() - print() # add newline + print() # add newline \ No newline at end of file diff --git a/lib/backends.py b/src/velocity/_backends.py similarity index 94% rename from lib/backends.py rename to src/velocity/_backends.py index cd169ae..83918c0 100644 --- a/lib/backends.py +++ b/src/velocity/_backends.py @@ -1,12 +1,13 @@ import re from hashlib import sha256 from pathlib import Path -from lib.exceptions import * from abc import ABC, abstractmethod +from ._exceptions import (UndefinedVariableInTemplate, RepeatedSection, LineOutsideOfSection, TemplateSyntaxError, + BackendNotSupported) +from ._config import config def _substitute(text: str, variables: dict, regex: str) -> str: - def _replace(m: re.Match): """ Substitute a variables in a string by a regex. @@ -24,7 +25,9 @@ class Backend(ABC): def __init__(self, name: str, variables: dict = None) -> None: self.name = name self.variables = { - '__backend__': self.name + '__backend__': config.get("velocity:backend"), + '__system__': config.get("velocity:system"), + '__distro__': config.get("velocity:distro") } if variables is not None: self.variables.update(variables) @@ -98,7 +101,7 @@ def _load_template(self, file: Path, variables: dict) -> list: _substitute( line, variables, - r'(? list: if '@from' not in sections: raise TemplateSyntaxError("You must have a '@from' section in your template!") elif len(sections['@from']) != 1: - raise TemplateSyntaxError("You can only have one source in your template!",) + raise TemplateSyntaxError("You can only have one source in your template!", ) elif len(sections['@from'][0].split()) != 1: raise TemplateSyntaxError("Your source must be a single string!", sections['@from'][0]) else: @@ -269,7 +272,7 @@ def generate_script(self, file: Path, variables: dict) -> list: elif len(sections['@from'][0].split()) != 1: raise TemplateSyntaxError("Your source must be a single string!", sections['@from'][0]) else: - if re.match(r'^.*\.sif$', sections['@from'][0]): #Path(sections['@from'][0]).is_file(): + if re.match(r'^.*\.sif$', sections['@from'][0]): #Path(sections['@from'][0]).is_file(): script.append('Bootstrap: localimage') elif re.match(r'^.*\/.*:.*$', sections['@from'][0]): script.append('Bootstrap: docker') @@ -351,10 +354,11 @@ def clean_up_old_image_cmd(self, name: str) -> str: return 'echo' -def get_backend(name: str, variables: dict) -> Backend: - if name == 'podman': +def get_backend(variables: dict) -> Backend: + backend = config.get("velocity:backend") + if backend == 'podman': return Podman(variables) - elif name == 'apptainer': + elif backend == 'apptainer': return Apptainer(variables) else: - raise BackendNotSupported(name) + raise BackendNotSupported(backend) diff --git a/lib/build.py b/src/velocity/_build.py similarity index 66% rename from lib/build.py rename to src/velocity/_build.py index df9635d..f84381d 100644 --- a/lib/build.py +++ b/src/velocity/_build.py @@ -10,10 +10,11 @@ from threading import Thread from pathlib import Path from colorama import Fore, Style -from lib.graph import Node -from lib.print import p1print, sp1print, TextBlock -from lib.exceptions import BackendNotSupported -from lib.backends import get_backend + +from ._config import config +from ._graph import Image +from ._print import p1print, sp1print, TextBlock +from ._backends import get_backend def read_pipe(pipe: PIPE, topic: SimpleQueue, prefix: str, log: SimpleQueue) -> None: @@ -91,58 +92,56 @@ def run(cmd: str, log_file: Path = None, verbose: bool = False) -> None: ]) exit(process.poll()) - +""" class BuildUnit: - def __init__(self, node: Node): + def __init__(self, node: Image): self.node = node self.build_id = ''.join(random.choice(string.ascii_lowercase) for _ in range(8)) - +""" class Builder: - def __init__(self, bt: tuple[Node], backend: str, system: str, distro: str, build_dir: str, - build_name: str = None, dry_run: bool = False, leave_tags: bool = True, verbose: bool = False): + def __init__(self, bt: tuple[Image], build_name: str = None, dry_run: bool = False, + leave_tags: bool = True, verbose: bool = False): self.build_units = list() - self.backend = backend - self.system = system - self.distro = distro - self.build_dir = Path(build_dir) self.build_name = build_name self.dry_run = dry_run self.leave_tags = leave_tags self.verbose = verbose - self.backend_engine = get_backend(self.backend, {'__system__': self.system, '__distro__': self.distro}) + self.backend_engine = get_backend({}) # create build_dir if it does not exist + self.build_dir = Path(config.get("velocity:build_dir")) self.build_dir.mkdir(mode=0o777, parents=True, exist_ok=True) - for node in bt: - self.build_units.append(BuildUnit(node)) + self.build_units = bt def build(self): # store pwd pwd = Path(Path().absolute()) - # clear build_dir + + """# clear build_dir for entry in self.build_dir.iterdir(): if entry.is_dir(): shutil.rmtree(entry) else: entry.unlink() + """ last = None # last image that was built for u in self.build_units: if u == self.build_units[-1]: - sorted_specs = [(bu.node.name, bu.node.tag) for bu in self.build_units] + sorted_specs = [(bu.name, bu.version) for bu in self.build_units] sorted_specs.sort() tag = str(self.build_name if self.build_name is not None else - f"{'_'.join(f'{bu[0]}-{bu[1]}' for bu in sorted_specs)}__{self.system}-{self.distro}") + f"{'_'.join(f'{bu[0]}-{bu[1]}' for bu in sorted_specs)}__{config.get('velocity:system')}-{config.get('velocity:distro')}") name = self.backend_engine.format_image_name(Path(pwd.absolute()), tag) else: - name = self.backend_engine.format_image_name(Path.joinpath(self.build_dir, u.build_id), u.build_id) + name = self.backend_engine.format_image_name(Path.joinpath(self.build_dir, u.id), u.id) self._build_image(u, last, name) if not self.dry_run and not self.leave_tags and last is not None: @@ -152,46 +151,46 @@ def build(self): # go back to the starting dir os.chdir(pwd) - def _build_image(self, unit: BuildUnit, src_image: str, name: str): + def _build_image(self, unit: Image, src_image: str, name: str): # print start of build p1print([ - TextBlock(f"{unit.build_id}", fore=Fore.RED, style=Style.BRIGHT), + TextBlock(f"{unit.id}", fore=Fore.RED, style=Style.BRIGHT), TextBlock(f": BUILD "), - TextBlock(f"{unit.node.name}@={unit.node.tag}", fore=Fore.MAGENTA, style=Style.BRIGHT), + TextBlock(f"{unit.name}@{unit.version}", fore=Fore.MAGENTA, style=Style.BRIGHT), TextBlock(f"{' --DRY-RUN' if self.dry_run else ''} ...") ]) start = timer() # create build dir and go to it - build_sub_dir = Path.joinpath(self.build_dir, unit.build_id) - build_sub_dir.mkdir(mode=0o744) + build_sub_dir = Path.joinpath(self.build_dir, unit.id) + build_sub_dir.mkdir(mode=0o744, exist_ok=True) os.chdir(build_sub_dir) # copy additional files - if Path.joinpath(unit.node.path, self.system).is_dir(): + if len(unit.files) > 0 and Path.joinpath(unit.path, "files").is_dir(): p1print([ - TextBlock(f"{unit.build_id}", fore=Fore.RED, style=Style.BRIGHT), + TextBlock(f"{unit.id}", fore=Fore.RED, style=Style.BRIGHT), TextBlock(f": COPYING FILES ...") ]) - for entry in Path.joinpath(unit.node.path, self.system).iterdir(): + for entry in unit.files: # print copy operation if self.verbose: sp1print([ - TextBlock('DIR: ' if entry.is_dir() else 'FILE: ', fore=Fore.YELLOW, style=Style.BRIGHT), - TextBlock(f"{entry.absolute()}", fore=Fore.GREEN), + TextBlock('DIR: ' if Path.joinpath(unit.path, "files", entry).is_dir() else 'FILE: ', fore=Fore.YELLOW, style=Style.BRIGHT), + TextBlock(f"{Path.joinpath(unit.path, 'files', entry).absolute()}", fore=Fore.GREEN), TextBlock(f" -> ", fore=Fore.YELLOW, style=Style.BRIGHT), - TextBlock(f"{Path.joinpath(build_sub_dir, entry.name).absolute()}", fore=Fore.GREEN) + TextBlock(f"{Path.joinpath(build_sub_dir, entry).absolute()}", fore=Fore.GREEN) ]) - if entry.is_dir(): - shutil.copytree(entry, Path.joinpath(build_sub_dir, entry.name)) + if Path.joinpath(unit.path, "files", entry).is_dir(): + shutil.copytree(Path.joinpath(unit.path, "files", entry), Path.joinpath(build_sub_dir, entry)) else: - shutil.copy(entry, Path.joinpath(build_sub_dir, entry.name)) + shutil.copy(Path.joinpath(unit.path, "files", entry), Path.joinpath(build_sub_dir, entry)) # parse template and create script... p1print([ - TextBlock(f"{unit.build_id}", fore=Fore.RED, style=Style.BRIGHT), + TextBlock(f"{unit.id}", fore=Fore.RED, style=Style.BRIGHT), TextBlock(f": GENERATING SCRIPT ...") ]) if self.verbose: @@ -201,17 +200,16 @@ def _build_image(self, unit: BuildUnit, src_image: str, name: str): ]) # get and update script variables - script_variables = unit.node.build_specifications['variables'] \ - if 'variables' in unit.node.build_specifications else dict() - script_variables.update({'__name__': unit.node.name}) - script_variables.update({'__tag__': unit.node.tag}) - script_variables.update({'__timestamp__': datetime.datetime.now()}) - script_variables.update({'__threads__': int(os.cpu_count() * 0.75) if int(os.cpu_count() * 0.75) < 16 else 16}) + script_variables = unit.variables.copy() + script_variables.update({'__name__': unit.name}) + script_variables.update({'__version__': str(unit.version)}) + script_variables.update({'__timestamp__': str(datetime.datetime.now())}) + script_variables.update({'__threads__': str(int(os.cpu_count() * 0.75) if int(os.cpu_count() * 0.75) < 16 else 16)}) if src_image is not None: script_variables.update({'__base__': src_image}) script = self.backend_engine.generate_script( - Path.joinpath(unit.node.path, 'templates', f'{self.distro}.vtmp'), + Path.joinpath(unit.path, 'templates', f'{unit.template}.vtmp'), script_variables ) # write out script @@ -227,26 +225,23 @@ def _build_image(self, unit: BuildUnit, src_image: str, name: str): build_cmd = self.backend_engine.generate_build_cmd( str(Path.joinpath(build_sub_dir, 'script')), name, - unit.node.build_specifications['arguments'] if 'arguments' in unit.node.build_specifications else None + unit.arguments ) build_file_path = Path.joinpath(build_sub_dir, 'build') build_contents = ['#!/usr/bin/env bash'] - if 'prolog' in unit.node.build_specifications: + + if unit.prolog is not None: p1print([ - TextBlock(f"{unit.build_id}", fore=Fore.RED, style=Style.BRIGHT), + TextBlock(f"{unit.id}", fore=Fore.RED, style=Style.BRIGHT), TextBlock(f": RUNNING PROLOG ... && BUILDING ...") ]) - with open(Path.joinpath(build_sub_dir, unit.node.build_specifications['prolog']), 'r') as prolog: - for line in prolog: - striped = re.sub(r'#.*', '', line) - if not striped.isspace(): - build_contents.append(striped.strip('\n')) + build_contents.append(unit.prolog.strip('\n')) build_contents.append(build_cmd) else: p1print([ - TextBlock(f"{unit.build_id}", fore=Fore.RED, style=Style.BRIGHT), + TextBlock(f"{unit.id}", fore=Fore.RED, style=Style.BRIGHT), TextBlock(f": BUILDING ...") ]) build_contents.append(build_cmd) @@ -267,11 +262,11 @@ def _build_image(self, unit: BuildUnit, src_image: str, name: str): end = timer() p1print([ - TextBlock(f"{unit.build_id}", fore=Fore.RED, style=Style.BRIGHT), + TextBlock(f"{unit.id}", fore=Fore.RED, style=Style.BRIGHT), TextBlock(f": IMAGE "), TextBlock(f"{name}", fore=Fore.MAGENTA, style=Style.BRIGHT), TextBlock(' ('), - TextBlock(f"{unit.node.name}@={unit.node.tag}", fore=Fore.MAGENTA, style=Style.BRIGHT), + TextBlock(f"{unit.name}@{unit.version}", fore=Fore.MAGENTA, style=Style.BRIGHT), TextBlock(f") BUILT ["), TextBlock(f"{datetime.timedelta(seconds=round(end - start))}", fore=Fore.MAGENTA, style=Style.BRIGHT), TextBlock(']') diff --git a/src/velocity/_config.py b/src/velocity/_config.py new file mode 100644 index 0000000..502b662 --- /dev/null +++ b/src/velocity/_config.py @@ -0,0 +1,257 @@ +from loguru import logger +from ._exceptions import InvalidSchema, InvalidConfig +from platform import processor as arch +from pathlib import Path +from os import getlogin as os_getlogin + +""" +Allowed schema configurations: + +bool: + required: bool=True + default: bool + +str: + required: bool=True + default: str + allowed: list[str]=None # None is treated as if there are no restrictions + +list: + required: bool=True + default: list + items: {schema} # the types of items in the list (bool or str) + +dict: + required: bool=True + auto: bool=False # if not defined auto-generate + cardinality: str="one" # allowed values are 'one' or 'many' + # If many, the dict name is ignored and n matches of the dict are allowed + properties: dict +""" + +DEFAULT_BOOL_SCHEMA = { + "type": bool, +} + +DEFAULT_STR_SCHEMA = { + "type": str, +} + +DEFAULT_LIST_SCHEMA = { + "type": list, + "default": list(), + "items": { + "type": str, + "required": False + } +} + +IM_SCHEMA = { + "type": str, + "default": "COMBINE", + "allowed": ["COMBINE", "OVERRIDE"] +} + +VELOCITY_SCHEMA = { + "type": dict, + "properties": { + "velocity": { + "type": dict, + "properties": { + "backend": DEFAULT_STR_SCHEMA, + "distro:": DEFAULT_STR_SCHEMA, + "system": DEFAULT_STR_SCHEMA, + "verbose": { + "type": bool, + "default": False + }, + "debug": { + "type": str, + "default": "INFO", + "allowed": ["INFO", "DEBUG"] + } + } + }, + "build_specs": { + "type": dict, + "default": { + "type": dict, + "auto": True, + "properties": { + "variables": DEFAULT_LIST_SCHEMA + } + }, + "system": { + "type": dict, + "required": False, + "cardinality": "many", + "properties": { + "default": { + "type": dict, + "auto": True, + "properties": { + "prolog_lines": DEFAULT_LIST_SCHEMA, + "IM_prolog_lines": IM_SCHEMA, + "variables": DEFAULT_LIST_SCHEMA, + "IM_variables": IM_SCHEMA + } + }, + "backend": { + "type": dict, + "required": False, + "cardinality": "many", + "properties": { + "default": { + "type": dict, + "auto": True, + "properties": { + "arguments": DEFAULT_LIST_SCHEMA, + "IM_arguments": IM_SCHEMA, + "prolog_lines": DEFAULT_LIST_SCHEMA, + "IM_prolog_lines": IM_SCHEMA, + "variables": DEFAULT_LIST_SCHEMA, + "IM_variables": IM_SCHEMA + } + }, + "image": { + "type": dict, + "required": False, + "cardinality": "many", + "properties": { + "arguments": DEFAULT_LIST_SCHEMA, + "IM_arguments": IM_SCHEMA, + "dependencies": DEFAULT_LIST_SCHEMA, + "IM_dependencies": IM_SCHEMA, + "prolog_script": { + "type": str, + "default": None + }, + "variables": DEFAULT_LIST_SCHEMA, + "IM_variables": IM_SCHEMA + } + } + } + } + } + } + } + } +} + + +def validate_and_generate_config(config: dict | list | str | bool | None, schema: dict) -> bool: + try: + if schema["type"] == dict: + allowed = {"type", "required", "auto", "cardinality", "properties"} + if not allowed.issuperset(schema.keys()): + raise InvalidSchema("schema {} contains invalid attributes: {}".format( + schema, + set(schema).difference(allowed) + )) + if not isinstance(schema["type"], type) or \ + not isinstance(schema["required"], bool) or \ + not isinstance(schema["auto"], bool) or \ + schema["cardinality"] not in ("one", "many") or \ + not (isinstance(schema["properties"], dict) and schema["properties"] is not None): + raise InvalidSchema("schema {} contains invalid attribute types".format( + schema + )) + if not isinstance(config, dict) and config is not None: + raise InvalidConfig("config type '{}' does not match schema type '{}'".format( + type(config), + schema["type"] + )) + + if schema["required"]: + if config is None: + raise InvalidConfig("schema required but config is '{}'".format( + None + )) + else: + for p in config.keys(): + for s in schema["properties"]: + validate_and_generate_config(config[p], s) + else: + if config is None: + return True + + + # return True + elif schema["type"] == list: + pass + elif schema["type"] == str: + pass + elif schema["type"] == bool: + pass + else: + raise InvalidSchema("schema type '{}' is not valid".format(schema["type"])) + + except (KeyError, TypeError): + logger.warning("schema {} is not a dict or does not specify type".format(schema)) + except (InvalidSchema, InvalidConfig) as e: + logger.warning(e) + return False + + +class Config: + """Configuration class.""" + def __init__(self) -> None: + self._config = dict() + + def set(self, item: str, value: int | bool | str | list | dict | None) -> None: + """Set config property.""" + try: + if item != "": + parts: list[str] = item.split(":") + set_value = self._config + for p in parts: + if not p.isidentifier(): + logger.warning("'{}' is not a valid identifier", format(item)) + if p != parts[-1]: + if p not in set_value: + set_value[p] = dict() + set_value = set_value[p] + else: + set_value[p] = value + else: + raise AttributeError("you cannot set the base config node") + except AttributeError as e: + logger.warning(e) + + def get(self, item: str) -> int | bool | str | list | dict | None: + """Get config property.""" + try: + if item != "": + parts: list[str] = item.split(":") + ret_value = self._config + for p in parts: + if not p.isidentifier(): + logger.warning("'{}' is not a valid identifier", format(item)) + return None + ret_value = ret_value[p] + return ret_value + else: + return self._config + except (KeyError, TypeError): + logger.warning("could not find '{}' in config", format(item)) + return None + + def __str__(self) -> str: + return str(self._config) + + +# default configuration +_config = Config() +_config.set("velocity", { + "system": arch(), + "backend": "apptainer", + "distro": "ubuntu", + "verbose": False, + "debug": "WARNING", + "config_dir": Path.home().joinpath(".velocity"), + "build_dir": Path("/tmp").joinpath(os_getlogin(), "velocity") +}) + + +# config singleton +config = _config diff --git a/lib/exceptions.py b/src/velocity/_exceptions.py similarity index 77% rename from lib/exceptions.py rename to src/velocity/_exceptions.py index c6392cf..472e227 100644 --- a/lib/exceptions.py +++ b/src/velocity/_exceptions.py @@ -49,3 +49,21 @@ class TemplateSyntaxError(Exception): def __init__(self, message, line: str = None): super().__init__(f"{message} {f':line: <{line}>' if line is not None else ''}") + + +class InvalidImageVersionError(Exception): + """Invalid image version spec.""" + def __init__(self, *args): + super().__init__(*args) + + +class InvalidSchema(Exception): + """Invalid schema.""" + def __init__(self, *args): + super().__init__(*args) + + +class InvalidConfig(Exception): + """Invalid config.""" + def __init__(self, *args): + super().__init__(*args) diff --git a/src/velocity/_graph.py b/src/velocity/_graph.py new file mode 100644 index 0000000..1111c17 --- /dev/null +++ b/src/velocity/_graph.py @@ -0,0 +1,752 @@ +"""Graph library and tools for dependency graph""" +from loguru import logger +from re import split as re_split, fullmatch as re_fullmatch, Match as ReMatch +from ._exceptions import ( + InvalidImageVersionError, + InvalidDependencySpecification, + EdgeViolatesDAG, + NoAvailableBuild +) +from pathlib import Path +from hashlib import sha256 +from typing_extensions import Self +from networkx import ( + DiGraph as nx_DiGraph, + is_directed_acyclic_graph as nx_is_directed_acyclic_graph, + find_cycle as nx_find_cycle, + neighbors as nx_neighbors, + has_path as nx_has_path +) +from yaml import safe_load as yaml_safe_load +from ._config import config +from copy import deepcopy +from enum import Enum + +VERSION_REGEX = \ + r"^(?P[0-9]+)(?:\.(?P[0-9]+)(:?\.(?P[0-9]+))?)?(?:-(?P[a-zA-Z0-9]+))?$" + + +def get_permutations(idx: int, sets: list[list]): + """ + Get all the possible permutations from a list of lists + :param idx: recursion level + :param sets: sets for permutation + :return: permutations + """ + permutations = list() + if len(sets) == 0: + pass + elif idx == len(sets) - 1: + for i in sets[idx]: + permutations.append({i}) + else: + for i in sets[idx]: + sub_permutations = get_permutations(idx + 1, sets) + for sub in sub_permutations: + permutations.append(sub.union({i})) + return permutations + + +class Version: + """Version class for images.""" + def __init__(self, version_specifier: str) -> None: + self.vs = version_specifier + try: + version_dict: dict = re_fullmatch(VERSION_REGEX, version_specifier).groupdict() + self.major: int | None = int(version_dict['major']) if version_dict['major'] is not None else None + self.minor: int | None = int(version_dict['minor']) if version_dict['minor'] is not None else None + self.patch: int | None = int(version_dict['patch']) if version_dict['patch'] is not None else None + self.suffix: str | None = str(version_dict['suffix']) if version_dict['suffix'] is not None else None + logger.trace("Version: {}".format(self.__str__())) + except AttributeError: + self.major: int | None = None + self.minor: int | None = None + self.patch: int | None = None + self.suffix: str | None = None + except ValueError: + logger.error("Could not parse version string: {}".format(version_specifier)) + exit(1) + + @property + def vcs(self) -> str: + """Version Comparison String""" + return "{:#>9}.{:#>9}.{:#>9}.{:~<9}".format( + self.major if self.major is not None else "#", + self.minor if self.minor is not None else "#", + self.patch if self.patch is not None else "#", + self.suffix if self.suffix is not None else "~" + ) + + def preferred(self, other) -> bool: + """Determine which version to prefer when two version are "equal" ex. 12.3 vs 12.3.0-rc1""" + return self.vcs > other.vcs + + @property + def _vcs_t(self) -> str: + """Version Comparison String Truncated""" + vl: int = 0 + if self.major is None: + pass + elif self.minor is None: + vl = 9 + elif self.patch is None: + vl = 19 + elif self.suffix is None: + vl = 29 + else: + vl = 39 + return self.vcs[:vl] + + @classmethod + def _cut_vcses_to_size(cls, one: Self, two: Self) -> tuple[str, str]: + """Cut vcses to the longest common length""" + length: int = min(len(one._vcs_t), len(two._vcs_t)) + return one._vcs_t[:length], two._vcs_t[:length] + + def __eq__(self, other) -> bool: + if not isinstance(other, Version): + raise TypeError(f"'>' not supported between instances of " + f"'{type(self).__name__}' and '{type(other).__name__}'") + s_vcs, o_vcs = self._cut_vcses_to_size(self, other) + return s_vcs == o_vcs + + def __ne__(self, other) -> bool: + if not isinstance(other, Version): + raise TypeError(f"'>' not supported between instances of " + f"'{type(self).__name__}' and '{type(other).__name__}'") + s_vcs, o_vcs = self._cut_vcses_to_size(self, other) + return s_vcs != o_vcs + + def __gt__(self, other) -> bool: + if not isinstance(other, Version): + raise TypeError(f"'>' not supported between instances of " + f"'{type(self).__name__}' and '{type(other).__name__}'") + return self.vcs > other.vcs and not self.__eq__(other) + + def __ge__(self, other) -> bool: + if not isinstance(other, Version): + raise TypeError(f"'>' not supported between instances of " + f"'{type(self).__name__}' and '{type(other).__name__}'") + return self.vcs > other.vcs or self.__eq__(other) + + def __lt__(self, other) -> bool: + if not isinstance(other, Version): + raise TypeError(f"'>' not supported between instances of " + f"'{type(self).__name__}' and '{type(other).__name__}'") + return self.vcs < other.vcs and not self.__eq__(other) + + def __le__(self, other) -> bool: + if not isinstance(other, Version): + raise TypeError(f"'>' not supported between instances of " + f"'{type(self).__name__}' and '{type(other).__name__}'") + return self.vcs < other.vcs or self.__eq__(other) + + def __str__(self) -> str: + return self.vs + + +class Spec: + def __init__(self): + pass + + +class Image: + def __init__(self, name: str, version: str, system: str, backend: str, distro: str, path: str) -> None: + # foundational + self.name: str = name + self.version: Version = Version(str(version)) + self.system: str = system + self.backend: str = backend + self.distro: str = distro + self.dependencies: list[str] = list() + + # additional + self.variables: dict[str, str] = dict() + self.arguments: list[str] = list() + self.template: str = "default" + self.files: list[str] = list() + self.prolog: str | None = None + + # metadata + self.path: Path = Path(path) + + def satisfies(self, spec: str) -> bool: + """Test if this node satisfies the given spec.""" + name_version_regex: str = \ + r"^(?P{})(?:(?:@(?P[\d\.]+)(?!@))?(?:@?(?P:)(?P[\d\.]+)?)?)?$".format(self.name) + system_regex: str = r"^system=(?P[a-zA-Z0-9]+)$" + backend_regex: str = r"^backend=(?P[a-zA-Z0-9]+)$" + distro_regex: str = r"^distro=(?P[a-zA-Z0-9]+)$" + dependency_regex: str = r"^\^(?P[a-zA-Z0-9-]+)$" + + ss: list[str] = re_split(r"\s+", spec.strip()) + + for part in ss: + # name and version + res: ReMatch | None = re_fullmatch(name_version_regex, part) + if res is not None: + gd: dict = res.groupdict() + if gd["left"] is not None and gd["right"] is None: # n@v: or n@v + if gd["colen"] is not None: + if Version(gd["left"]) > self.version: + return False + else: + if Version(gd["left"]) != self.version: + return False + elif gd["left"] is None and gd["right"] is not None: # n@:v + if gd["colen"] is not None: + if Version(gd["right"]) < self.version: + return False + else: + return False + elif gd["left"] is None and gd["right"] is None: # n + if gd["colen"] is not None: + return False + else: # n@v:v + if Version(gd["left"]) > self.version or self.version > Version(gd["right"]): + return False + continue # part has been handled so continue + + # system + res = re_fullmatch(system_regex, part) + if res is not None: + if res.group("system") != self.system: + return False + continue # part has been handled so continue + + # backend + res = re_fullmatch(backend_regex, part) + if res is not None: + if res.group("backend") != self.backend: + return False + continue # part has been handled so continue + + # distro + res = re_fullmatch(distro_regex, part) + if res is not None: + if res.group("distro") != self.distro: + return False + continue # part has been handled so continue + + # dependencies + res = re_fullmatch(dependency_regex, part) + if res is not None: + matched = False + for dep in self.dependencies: + if res.group("name") == dep: + matched = True + if matched: + continue # match is found so continue + + # if we get here this part has not been handled + return False + + # all parts were handled + return True + + def apply_constraint(self, conditional: str, _type: str, spec: str) -> bool: + if self.satisfies(conditional): + if _type == "dependency": + self.dependencies.append(spec) + elif _type == "variable": + parts = spec.split("=") + self.variables[parts[0]] = parts[1] + elif _type == "argument": + self.arguments.append(spec) + elif _type == "template": + self.template = spec + elif self.files == "file": + self.files.append(spec) + elif _type == "prolog": + self.prolog = spec + else: + # not a valid type + return False + else: + # spec not added + return False + + @property + def hash(self) -> str: + """Return a hash for this node uniquely identifying it.""" + + hash_list: list = list() + hash_list.append(self.name) + hash_list.append(self.version) + hash_list.append(self.system) + hash_list.append(self.backend) + hash_list.append(self.distro) + hash_list.append(",".join(str(x) for x in self.dependencies)) + hash_list.append(",".join(str(x) for x in self.variables)) + hash_list.append(",".join(str(x) for x in self.arguments)) + tf = Path(self.path).joinpath("templates", self.template) + if tf.is_file(): + hash_list.append(sha256(tf.read_bytes()).hexdigest()) + else: + hash_list.append(None) + hash_list.append(",".join(str(x) for x in self.files)) + hash_list.append(self.prolog) + + hash_str: str = "|".join(str(x) for x in hash_list) + return sha256(hash_str.encode()).hexdigest() + + @property + def id(self) -> str: + return self.hash[:7] + + def __hash__(self) -> int: + return int(self.hash, 16) + + def __eq__(self, other) -> bool: + if not isinstance(other, Image): + return False + return self.hash == other.hash + + def __lt__(self, other) -> bool: + if isinstance(other, Image): + return not self.version.preferred(other.version) + return False + + def __str__(self) -> str: + return "{} {}@{} system={} backend={} distro={}{}".format( + self.hash[:7], + self.name, + self.version, + self.system, + self.backend, + self.distro, + "".join(" ^{}".format(x) for x in self.dependencies) if len(self.dependencies) > 0 else "" + ) + + +class DepOp(Enum): + """ + Specify dependency options + """ + EQ = '=' + GE = '>=' + LE = '<=' + UN = None + + +class Target: + """ + Build specification targets + """ + + def __init__(self, node: Image, op: DepOp): + self.node = node + self.op = op + + def __str__(self): + return f'Target: {self.op} -> {str(self.node)}' + + +class ImageGraph(nx_DiGraph): + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + + def add_edge(self, u_of_edge: Image, v_of_edge: Image, **attr) -> None: + # check that edge endpoint are in graph + if self.has_node(u_of_edge) and self.has_node(v_of_edge): + super().add_edge(u_of_edge, v_of_edge, **attr) + else: + raise InvalidDependencySpecification(f'{v_of_edge.name}@{v_of_edge.version}', + u_of_edge.name, u_of_edge.version, + Path.joinpath(u_of_edge.path, 'specs.yaml')) + + # check that graph is still a DAG + if not nx_is_directed_acyclic_graph(self): + cycle = nx_find_cycle(self) + raise EdgeViolatesDAG(u_of_edge, v_of_edge, cycle) + + def get_similar_nodes(self, node: Image) -> set: + similar = set() + for n in self.nodes: + if n.satisfies(node.name): + similar.add(n) + return similar + + def get_dependencies(self, node: Image) -> set[Image]: + # had to add this wrapper because nx.neighbors was dropping the attributes of some nodes (cuda, python) + deps = set() + for node in nx_neighbors(self, node): + for n in self.nodes: + if n == node: + deps.add(n) + # will contact the maintainers of networkx at some point + + return deps + + def is_above(self, u_node: Image, v_node: Image) -> bool: + """ + Test if one node is above another in the dependency tree + """ + return nx_has_path(self, u_node, v_node) + + def _is_valid_build_tuple(self, bt: tuple[Image]) -> bool: + """ + Verify that all the dependencies of a build tuple can be met. + """ + valid = True + + # check for similar images + for i0 in range(len(bt)): + for i2 in bt[i0 + 1:]: + if bt[i0].satisfies(i2.name): + valid = False + + # check that all images exist + for node in bt: + if not self.has_node(node): + valid = False + + # break prematurely if the first two checks fail + if not valid: + return valid + + # check that deps in build tuple + for node in bt: + deps = self.get_dependencies(node) + + # group deps + grouped = dict() + for d in deps: + if d.name not in grouped: + grouped[d.name] = set() + grouped[d.name].add(d) + + # check that the needed dependency exists + for g in grouped: + if grouped[g].isdisjoint(bt): + valid = False + + return valid + + def create_build_recipe(self, targets: list[Target]) -> tuple: + + # check if all the targets exist + for node in targets: + if len(self.get_similar_nodes(node.node)) < 1: + raise NoAvailableBuild(f"The build target {node.node} does not exist!") + + # init build set and priority list + build_set = set() + priority_list = list() + + # add similar to build set + for target in targets: + build_set.update(self.get_similar_nodes(target.node)) + # make sure to update priority + if target.node.name not in priority_list: + priority_list.append(target.node.name) + + # add deps to build set + while True: + build_set_length = len(build_set) + + for node in build_set.copy(): + dependencies = self.get_dependencies(node) + for d in dependencies: + build_set.add(d) + if d.name not in priority_list: + priority_list.append(d.name) + + # loop until all dependencies are added + if build_set_length == len(build_set): + break + + # apply constraints + for target in targets: + for node in build_set.copy(): + if node.satisfies(target.node.name): + if target.op == DepOp.EQ and node.version != target.node.version: + build_set.remove(node) + elif target.op == DepOp.GE and node.version < target.node.version: + build_set.remove(node) + elif target.op == DepOp.LE and node.version > target.node.version: + build_set.remove(node) + + # group deps + grouped = dict() + for node in build_set: + if node.name not in grouped: + grouped[node.name] = set() + grouped[node.name].add(node) + # sort deps so that the highest versions of images further up the dep tree will be chosen + prioritized_list_group = list() + for group in priority_list: + tmp = list(grouped[group]) + tmp.sort(reverse=True) + prioritized_list_group.append(tmp) + # get permutations + permutations = get_permutations(0, prioritized_list_group) + + # return valid build tuple + for p in permutations: + + # clean up permutation + clean_p = set() + for n in p: + for t in targets: + con_targ = None + for nt in [x for x in p]: + if nt.satisfies(t.node.name): + con_targ = nt + break + if self.is_above(con_targ, n): + clean_p.add(n) + break + + if self._is_valid_build_tuple(tuple(clean_p)): + # order build + build_list = list() + processed = set() + unprocessed = clean_p.copy() + while len(unprocessed) > 0: + level_holder = list() + for node in unprocessed.copy(): + deps = set(self.get_dependencies(node)).intersection(clean_p) + if deps.issubset(processed): + level_holder.append(node) + level_holder.sort() + for node in level_holder: + processed.add(node) + unprocessed.remove(node) + build_list.append(node) + + return tuple(build_list) + + # if we got here no valid build tuple could be found + raise NoAvailableBuild("No Available build!") + + +class ImageRepo: + def __init__(self) -> None: + # constraint(image, condition, type, spec, scope) + self.constraints: list[tuple[str, str, str, str, str]] = list() + self.images: set[Image] = set() + + def import_from_dir(self, path: str) -> None: + """Add Images from path.""" + p = Path(path) + if not p.is_dir(): + raise NotADirectoryError(f"Image path {path} is not a directory!") + + for name in [x for x in p.iterdir() if x.is_dir() and x.name[0] != '.']: + with open(name.joinpath("specs.yaml"), "r") as fi: + specs = yaml_safe_load(fi) + # add versions + for version in specs["versions"]: + image = Image( + name.name, + version["spec"], + config.get("velocity:system"), + config.get("velocity:backend"), + config.get("velocity:distro"), + str(name) + ) + if "when" in version: + if image.satisfies(version["when"]): + self.images.add(image) + else: + self.images.add(image) + + # add constraints + # dependencies + if "dependencies" in specs: + for dependency in specs["dependencies"]: + self.constraints.append(( + name.name, + dependency["when"] if "when" in dependency else "", + "dependency", + dependency["spec"], + dependency["scope"] if "scope" in dependency else "image" + )) + # templates + if "templates" in specs: + for template in specs["templates"]: + self.constraints.append(( + name.name, + template["when"] if "when" in template else "", + "template", + template["path"], + template["scope"] if "scope" in template else "image" + )) + # arguments + if "arguments" in specs: + for argument in specs["arguments"]: + self.constraints.append(( + name.name, + argument["when"] if "when" in argument else "", + "argument", + argument["value"], + argument["scope"] if "scope" in argument else "image" + )) + # variables + if "variables" in specs: + for variable in specs["variables"]: + self.constraints.append(( + name.name, + variable["when"] if "when" in variable else "", + "variable", + "{}={}".format(variable["name"], variable["value"]), + variable["scope"] if "scope" in variable else "image" + )) + # files + if "files" in specs: + for file in specs["files"]: + self.constraints.append(( + name.name, + file["when"] if "when" in file else "", + "file", + file["name"], + file["scope"] if "scope" in file else "image" + )) + # prologs + if "prologs" in specs: + for prolog in specs["prologs"]: + self.constraints.append(( + name.name, + prolog["when"] if "when" in prolog else "", + "prolog", + prolog["script"], + prolog["scope"] if "scope" in prolog else "image" + )) + + def create_build_recipe(self, targets: list[str]) -> tuple: + images = deepcopy(self.images) + + build_targets = list() + targs = list() + for t in targets: + res = re_fullmatch( + r"^(?P[a-zA-Z0-9-]+)(?:(?:@(?P[\d\.]+)(?!@))?(?:@?(?P:)(?P[\d\.]+)?)?)?$", + t + ) + if res is not None: + gd: dict = res.groupdict() + if gd["left"] is not None and gd["right"] is None: # n@v: or n@v + if gd["colen"] is not None: + t = Image( + gd["name"], + gd["left"], + config.get("velocity:system"), + config.get("velocity:backend"), + config.get("velocity:distro"), + "" + ) + build_targets.append(Target( + t, + DepOp.GE + )) + targs.append(t) + else: + t = Image( + gd["name"], + gd["left"], + config.get("velocity:system"), + config.get("velocity:backend"), + config.get("velocity:distro"), + "" + ) + build_targets.append(Target( + t, + DepOp.EQ + )) + targs.append(t) + elif gd["left"] is None and gd["right"] is not None: # n@:v + if gd["colen"] is not None: + t = Image( + gd["name"], + gd["right"], + config.get("velocity:system"), + config.get("velocity:backend"), + config.get("velocity:distro"), + "" + ) + build_targets.append(Target( + t, + DepOp.LE + )) + targs.append(t) + else: + raise InvalidImageVersionError() + elif gd["left"] is None and gd["right"] is None: # n + if gd["colen"] is not None: + raise InvalidImageVersionError() + else: + t = Image( + gd["name"], + "", + config.get("velocity:system"), + config.get("velocity:backend"), + config.get("velocity:distro"), + "" + ) + build_targets.append(Target( + t, + DepOp.EQ + )) + targs.append(t) + else: # n@v:v + t = Image( + gd["name"], + gd["left"], + config.get("velocity:system"), + config.get("velocity:backend"), + config.get("velocity:distro"), + "" + ) + build_targets.append(Target( + t, + DepOp.GE + )) + targs.append(t) + t = Image( + gd["name"], + gd["right"], + config.get("velocity:system"), + config.get("velocity:backend"), + config.get("velocity:distro"), + "" + ) + build_targets.append(Target( + t, + DepOp.LE + )) + targs.append(t) + else: + raise NoAvailableBuild("No available build!") + + # apply constraints + for constraint in self.constraints: + if constraint[4] == "build": + for targ in targs: + if targ.satisfies(constraint[1]): + for image in images: + image.apply_constraint( + constraint[0], + constraint[2], + constraint[3] + ) + + else: # default is image + for image in images: + image.apply_constraint( + "{} {}".format(constraint[0], constraint[1]), + constraint[2], + constraint[3] + ) + + ig = ImageGraph() + for image in images: + ig.add_node(image) + for image in images: + for dep in image.dependencies: + for di in images: + if di.satisfies(dep): + ig.add_edge(image, di) + + return ig.create_build_recipe(build_targets) + diff --git a/lib/graph.py b/src/velocity/_graph_old.py similarity index 99% rename from lib/graph.py rename to src/velocity/_graph_old.py index 5c5f7ae..f3eadeb 100644 --- a/lib/graph.py +++ b/src/velocity/_graph_old.py @@ -3,7 +3,7 @@ import networkx as nx from enum import Enum from pathlib import Path -from lib.exceptions import EdgeViolatesDAG, InvalidDependencySpecification, NoAvailableBuild +from ._exceptions import EdgeViolatesDAG, InvalidDependencySpecification, NoAvailableBuild def get_permutations(idx: int, sets: list[list]): diff --git a/lib/print.py b/src/velocity/_print.py similarity index 100% rename from lib/print.py rename to src/velocity/_print.py diff --git a/lib/__init__.py b/tests/__init__.py similarity index 100% rename from lib/__init__.py rename to tests/__init__.py diff --git a/tests/test__config.py b/tests/test__config.py new file mode 100644 index 0000000..9ce92ee --- /dev/null +++ b/tests/test__config.py @@ -0,0 +1,59 @@ +from unittest import TestCase +from src.velocity._config import validate_and_generate_config + +TEST_SCHEMA = { + "type": dict, + "properties": { + "test_dict": { + "type": dict, + "properties": { + "test_list": {}, + "test_str": {}, + "test_bool": {} + } + }, + "missing_dict": {} + } +} + + +class Test(TestCase): + """ + def test_validate_config(self): + # misc + self.assertFalse(validate_and_generate_config({}, {})) + self.assertFalse(validate_and_generate_config(1, {})) + + # dict + ts = { + "type": dict, + "required": True, + "auto": False, + "cardinality": "one", + "properties": {} + } + self.assertTrue(validate_and_generate_config({}, ts)) + self.assertFalse(validate_and_generate_config(None, ts)) + self.assertFalse(validate_and_generate_config({"test": 0}, ts)) + + ts = { + "type": dict, + "required": True, + "auto": False, + "cardinality": "one", + "properties": { + "test": { + "type": bool + } + } + } + self.assertFalse(validate_and_generate_config({"test": True}, ts)) + + # list + + # str + + # bool + + self.fail() + """ diff --git a/tests/test__graph.py b/tests/test__graph.py new file mode 100644 index 0000000..e94a65f --- /dev/null +++ b/tests/test__graph.py @@ -0,0 +1,197 @@ +from unittest import TestCase +from re import compile +from src.velocity._graph import ( + VERSION_REGEX, + Version, + Image +) + + +class Test(TestCase): + def test_version_regex(self): + p = compile(VERSION_REGEX) + + # test 2.3.5-s90er + m = p.search("2.3.5-s90er") + self.assertEqual("2", m.group("major")) + self.assertEqual("3", m.group("minor")) + self.assertEqual("5", m.group("patch")) + self.assertEqual("s90er", m.group("suffix")) + + # test 2.3.5 + m = p.search("2.3.5") + self.assertEqual("2", m.group("major")) + self.assertEqual("3", m.group("minor")) + self.assertEqual("5", m.group("patch")) + self.assertIsNone(m.group("suffix")) + + # test 2.3 + m = p.search("2.3") + self.assertEqual("2", m.group("major")) + self.assertEqual("3", m.group("minor")) + self.assertIsNone(m.group("patch")) + self.assertIsNone(m.group("suffix")) + + # test 2 + m = p.search("2") + self.assertEqual("2", m.group("major")) + self.assertIsNone(m.group("minor")) + self.assertIsNone(m.group("patch")) + self.assertIsNone(m.group("suffix")) + + +class TestVersion(TestCase): + def test_init(self): + # test 2.3.5-s90er + v = Version("2.3.5-s90er") + self.assertEqual(2, v.major) + self.assertEqual(3, v.minor) + self.assertEqual(5, v.patch) + self.assertEqual("s90er", v.suffix) + + # test 2.3.5 + v = Version("2.3.5") + self.assertEqual(2, v.major) + self.assertEqual(3, v.minor) + self.assertEqual(5, v.patch) + self.assertIsNone(v.suffix) + + # test 2.3 + v = Version("2.3") + self.assertEqual(2, v.major) + self.assertEqual(3, v.minor) + self.assertIsNone(v.patch) + self.assertIsNone(v.suffix) + + # test 2 + v = Version("2") + self.assertEqual(2, v.major) + self.assertIsNone(v.minor) + self.assertIsNone(v.patch) + self.assertIsNone(v.suffix) + + def test_str(self): + # test 2.3.5-s90er + v = Version("2.3.5-s90er") + self.assertEqual("2.3.5-s90er", v.__str__()) + + # test 2.3.5 + v = Version("2.3.5") + self.assertEqual("2.3.5", v.__str__()) + + # test 2.3 + v = Version("2.3") + self.assertEqual("2.3", v.__str__()) + + # test 2 + v = Version("2") + self.assertEqual("2", v.__str__()) + + def test_comparisons(self): + # test 2.3.4 vs 2.3.4 + one = Version("2.3.4") + two = Version("2.3.4") + self.assertTrue(one == two) + self.assertFalse(one > two) + self.assertTrue(one >= two) + self.assertFalse(one < two) + self.assertTrue(one <= two) + + # test 2.3.4-rc1 vs 2.3.4-rc1 + one = Version("2.3.4-rc1") + two = Version("2.3.4-rc1") + self.assertTrue(one == two) + self.assertFalse(one > two) + self.assertTrue(one >= two) + self.assertFalse(one < two) + self.assertTrue(one <= two) + + # test 2.3.4 vs 2.3.5 + one = Version("2.3.4") + two = Version("2.3.5") + self.assertFalse(one == two) + self.assertFalse(one > two) + self.assertFalse(one >= two) + self.assertTrue(one < two) + self.assertTrue(one <= two) + + # test 2.3.6 vs 2.3.4 + one = Version("2.3.6") + two = Version("2.3.4") + self.assertFalse(one == two) + self.assertTrue(one > two) + self.assertTrue(one >= two) + self.assertFalse(one < two) + self.assertFalse(one <= two) + + # test 2.3.4 vs 2.3.4-rc1 + one = Version("2.3.4") + two = Version("2.3.4-rc1") + self.assertTrue(one == two) + self.assertFalse(one > two) + self.assertTrue(one >= two) + self.assertFalse(one < two) + self.assertTrue(one <= two) + + # test 2.3.4 vs 2.3 & 2 + one = Version("2.3.4") + two = Version("2.3") + self.assertTrue(one == two) + self.assertFalse(one > two) + self.assertTrue(one >= two) + self.assertTrue(one <= two) + self.assertFalse(one < two) + two = Version("2") + self.assertTrue(one == two) + self.assertFalse(one > two) + self.assertTrue(one >= two) + self.assertTrue(one <= two) + self.assertFalse(one < two) + + +class TestImage(TestCase): + def test_satisfies_spec(self): + image = Image("gcc", "12.3.0", "frontier", "apptainer", "ubuntu", "") + image2 = Image("test", "1", "frontier", "apptainer", "ubuntu", "") + + # name + self.assertTrue(image.satisfies("gcc")) + self.assertTrue(image2.satisfies("test")) + self.assertFalse(image.satisfies("ubuntu")) + + # version + self.assertTrue(image.satisfies("gcc@12.3.0")) + self.assertTrue(image.satisfies("gcc@12.3")) + self.assertTrue(image.satisfies("gcc@12")) + self.assertTrue(image.satisfies("gcc@12.3.0:")) + self.assertTrue(image.satisfies("gcc@:12.3.0")) + self.assertTrue(image.satisfies("gcc@11.0.0:13.0.0")) + self.assertTrue(image.satisfies("gcc@:12")) + self.assertTrue(image.satisfies("gcc gcc@12.3.0")) + + self.assertFalse(image.satisfies("gcc@14.0.0")) + self.assertFalse(image.satisfies("gcc@1")) + self.assertFalse(image.satisfies("ubuntu@12.3.0")) + self.assertFalse(image.satisfies("12.3.0")) + self.assertFalse(image.satisfies("gcc@13.0.0:13.2.0")) + + # system + self.assertTrue(image.satisfies("system=frontier")) + self.assertFalse(image.satisfies("system=summit")) + self.assertFalse(image.satisfies("frontier")) + + # backend + self.assertTrue(image.satisfies("backend=apptainer")) + self.assertFalse(image.satisfies("backend=podman")) + self.assertFalse(image.satisfies("apptainer")) + + # distro + self.assertTrue(image.satisfies("distro=ubuntu")) + self.assertFalse(image.satisfies("dictro=opensuse")) + self.assertFalse(image.satisfies("ubuntu")) + + #def test_add_spec(self): + # self.fail() + + #def test_hash(self): + # self.fail() From 1667dccca446451481557a6835d525bb3c8a74c1 Mon Sep 17 00:00:00 2001 From: Asa Rentschler Date: Fri, 6 Sep 2024 14:40:06 -0400 Subject: [PATCH 02/10] Cleaned up the backend and added new features --- pyproject.toml | 52 ++- src/velocity/__init__.py | 4 + src/velocity/_backends.py | 687 +++++++++++++++++++++---------------- src/velocity/_config.py | 251 ++------------ src/velocity/_graph_old.py | 519 ---------------------------- src/velocity/_print.py | 60 ++-- tests/test__config.py | 82 ++--- 7 files changed, 546 insertions(+), 1109 deletions(-) delete mode 100644 src/velocity/_graph_old.py diff --git a/pyproject.toml b/pyproject.toml index 491927a..e077167 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,9 +15,59 @@ dependencies = [ "networkx", "colorama", "editor", - "loguru" + "loguru", + "sys", + "shutil" ] [project.urls] Homepage = "https://github.com/olcf/velocity" Issues = "https://github.com/olcf/velocity/issues" + +[tool.ruff] +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] +line-length = 120 +indent-width = 4 +target-version = "py310" + +[tool.ruff.lint] +select = ["E4", "E7", "E9", "F"] +ignore = [] +fixable = ["ALL"] +unfixable = [] +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" +docstring-code-format = true +docstring-code-line-length = "dynamic" diff --git a/src/velocity/__init__.py b/src/velocity/__init__.py index 6402a0d..2da6386 100644 --- a/src/velocity/__init__.py +++ b/src/velocity/__init__.py @@ -3,3 +3,7 @@ __version__ = "0.0.0" config.set("velocity:version", __version__) + +__all__ = [ + config +] diff --git a/src/velocity/_backends.py b/src/velocity/_backends.py index 83918c0..61c2c5c 100644 --- a/src/velocity/_backends.py +++ b/src/velocity/_backends.py @@ -1,364 +1,451 @@ -import re -from hashlib import sha256 +"""Velocity backends.""" + +from re import Match as re_Match, sub as re_sub, match as re_match +from loguru import logger +from shutil import which as shutil_which from pathlib import Path from abc import ABC, abstractmethod -from ._exceptions import (UndefinedVariableInTemplate, RepeatedSection, LineOutsideOfSection, TemplateSyntaxError, - BackendNotSupported) +from ._exceptions import ( + RepeatedSection, + LineOutsideOfSection, + TemplateSyntaxError, + BackendNotSupported, + BackendNotAvailable, +) from ._config import config +from ._graph import Image + +def _substitute(text: str, variables: dict[str, str], regex: str) -> str: + """Substitute a variables in a string by a regex.""" -def _substitute(text: str, variables: dict, regex: str) -> str: - def _replace(m: re.Match): - """ - Substitute a variables in a string by a regex. - """ - if m.group(1) in variables: + def _replace(m: re_Match): + try: return str(variables[m.group(1)]) - else: - raise UndefinedVariableInTemplate(m.group(1)) + except KeyError: + logger.warning("The variable '{}' is undefined. Setting value to ''.".format(m.group(1))) - return re.sub(regex, _replace, text) + return re_sub(regex, _replace, text) class Backend(ABC): - - def __init__(self, name: str, variables: dict = None) -> None: - self.name = name - self.variables = { - '__backend__': config.get("velocity:backend"), - '__system__': config.get("velocity:system"), - '__distro__': config.get("velocity:distro") - } - if variables is not None: - self.variables.update(variables) - self.template_sections = [ - '@from', - '@pre', - '@arg', - '@copy', - '@run', - '@env', - '@label', - '@entry', - '@post' - ] - - @abstractmethod - def generate_script(self, file: Path, variables: dict) -> list: - """ - Generate a build script e.g. .dockerfile/.def - """ - pass - - @abstractmethod - def generate_build_cmd(self, src: str, dest: str, args: list = None) -> str: - """ - Generate CLI command to build image - """ - pass - - @abstractmethod - def format_image_name(self, path: Path, tag: str) -> str: - pass - - @abstractmethod - def clean_up_old_image_cmd(self, name: str) -> str: - pass - - def _get_sections(self, template: list) -> dict: - sections = dict() - - past_sections = list() - current_section = None + """Abstract class for velocity backend.""" + + template_sections: list[str] = [ + "@from", + "@pre", + "@copy", + "@run", + "@env", + "@label", + "@entry", + "@post", + ] + + @classmethod + def _get_sections(cls, template: list[str]) -> dict[str, list[str]]: + """Retrieve the sections from a VTMP.""" + + sections: dict[str, list[str]] = dict() + past_sections: list[str] = list() + current_section: str | None = None for line in template: - if line in self.template_sections: + # if a line contains a section header + if line in cls.template_sections: + # move the current section to past_sections if current_section is not None: past_sections.append(current_section) + # check for repeated section if line in past_sections: - raise RepeatedSection(line) + raise RepeatedSection(f"You have more than one '{line}' section in your template!") else: current_section = line if current_section not in sections: sections[current_section] = list() + else: + # handel content lines if current_section is None: - raise LineOutsideOfSection + raise LineOutsideOfSection("You have a line outside of a section in your template!") else: sections[current_section].append(line) + # remove empty sections + del_list: list[str] = list() + for sec in sections: + if len(sections[sec]) == 0: + del_list.append(sec) + for sec in del_list: + del sections[sec] + return sections - def _load_template(self, file: Path, variables: dict) -> list: - variables.update(self.variables) - template = list() - with open(file, 'r') as file: - contents = file.readlines() - variables.update({'__hash__': sha256(''.join(contents).encode('utf-8')).hexdigest()}) + @classmethod + def _filter_content(cls, image: Image, text: str) -> str: + """Filter conditionals and white space from a template line.""" + # handle conditionals + res: re_Match[str] = re_match(r".*(\?\?([\S ]*)\|>(.*)\?\?).*", text) + if res is not None: + if image.satisfies(res.group(2)): + text = re_sub(r".*(\?\?.*\?\?).*", res.group(3), text) + else: + text = "" + + # remove comments, newlines, and superfluous white space + text = re_sub(r"###.*", "", text) + text = re_sub(r"\n", "", text) + text = re_sub(r"^\s+|\s+$", "", text) + + return text + + @classmethod + def _load_template(cls, image: Image, variables: dict[str, str]) -> list[str]: + """Load a template and parse it.""" + template: list[str] = list() + with open( + Path(image.path).joinpath("templates", "{}.vtmp".format(image.template)), + "r", + ) as file: + contents: list[str] = file.readlines() for line in contents: - sf_con = self._filter_content( - _substitute( - line, - variables, - r'(? str: - # remove comments, newlines, and superfluous white space - t = re.sub(r'#.*', '', text) - t = re.sub(f'\n', '', t) - t = t.strip(' ') - - # is this line needed for this backend? - if t == '': - return '' - elif t[0] == '?' and f'?{self.name}' not in t: - return '' - else: - return re.sub(r'\?\w*', '', t).strip(' ') + def __init__(self, name: str, executable: str) -> None: + self.name: str = name + self.executable: str = executable + def generate_script(self, image: Image, variables: dict[str, str]) -> list[str]: + """Generate a build script e.g. .dockerfile/.def""" + template: list[str] = self._load_template(image, variables) + sections: dict[str, list[str]] = self._get_sections(template) + script: list[str] = list() -class Podman(Backend): + # @from + try: + if len(sections["@from"]) != 1: + raise TemplateSyntaxError( + "You can only have one source in the @from section!", + ) + elif len(sections["@from"][0].split()) != 1: + raise TemplateSyntaxError("Your source must be a single string!", sections["@from"][0]) + else: + script.extend(self._from(sections["@from"])) + except KeyError: + raise TemplateSyntaxError("You must have an @from section in your template!") + # arguments + script.extend(self._arguments(sections)) + # @pre + if "@pre" in sections: + script.extend(self._literal_section(sections["@pre"])) + # @copy + if "@copy" in sections: + script.extend(self._copy(sections["@copy"])) + # @run + if "@run" in sections: + env_ext: list[str] = list() + script.extend(self._run(sections["@run"], env_ext)) + if "@env" in sections: + sections["@env"].extend(env_ext) + else: + sections["@env"] = env_ext + # @env + if "@env" in sections: + script.extend(self._env(sections["@env"])) + # @label + if "@label" in sections: + script.extend(self._label(sections["@label"])) + # @entry + if "@entry" in sections: + if len(sections["@entry"]) != 1: + raise TemplateSyntaxError("You can only have one entrypoint!") + script.extend(self._entry(sections["@entry"])) + # @post + if "@post" in sections: + script.extend(self._literal_section(sections["@post"])) - def __init__(self, variables: dict): - super().__init__(name='podman', variables=variables) + return script - def generate_script(self, file: Path, variables: dict) -> list: - script = list() - template = self._load_template(file, variables) - sections = self._get_sections(template) + def is_available(self) -> bool: + """Check if the current system has the requested backend.""" + if shutil_which(self.executable) is None: + return False + return True - # @from - if '@from' not in sections: - raise TemplateSyntaxError("You must have a '@from' section in your template!") - elif len(sections['@from']) != 1: - raise TemplateSyntaxError("You can only have one source in your template!", ) - elif len(sections['@from'][0].split()) != 1: - raise TemplateSyntaxError("Your source must be a single string!", sections['@from'][0]) - else: - script.append(f"FROM {sections['@from'][0]}") - - # @arg - vbs = dict() - if '@arg' in sections: - if len(sections['@arg']) > 0: - script.append('') - for a in sections['@arg']: - if len(a.split()) != 1: - raise TemplateSyntaxError("Arguments cannot have spaces in their names!") - else: - script.append(f'ARG {a}') - vbs[a] = f'${a}' - for si in sections.keys(): - for li in range(len(sections[si])): - sections[si][li] = _substitute(sections[si][li], vbs, r'(? list[str]: + """Handle the @from section.""" - # @copy - if '@copy' in sections: - if len(sections['@copy']) > 0: - script.append('') - for ln in sections['@copy']: - if len(ln.split()) != 2: - raise TemplateSyntaxError("Your '@copy' can only have one source and destination!", ln) - script.append(f"COPY {ln}") - - if '@run' in sections: - if len(sections['@run']) > 0: - script.append('') - for cmd in sections['@run']: - ln = '' - if cmd == sections['@run'][0]: - ln += 'RUN ' - else: - ln += ' ' - ln += cmd - if cmd != sections['@run'][-1]: - ln += ' && \\' - script.append(ln) - - if '@env' in sections: - if len(sections['@env']) > 0: - script.append('') - for env in sections['@env']: - parts = env.split() - ln = '' - if env == sections['@env'][0]: - ln += 'ENV ' - else: - ln += ' ' - ln += f"{parts[0]}=\"{env.lstrip(parts[0]).strip(' ')}\"" - if env != sections['@env'][-1]: - ln += ' \\' - script.append(ln) - - if '@label' in sections: - if len(sections['@label']) > 0: - script.append('') - for label in sections['@label']: - parts = label.split() - if len(parts) != 2: - raise TemplateSyntaxError("Labels must have two parts!", label) - ln = '' - if label == sections['@label'][0]: - ln += 'LABEL ' - else: - ln += ' ' - ln += f"{parts[0]}=\"{label.lstrip(parts[0]).strip(' ')}\"" - if label != sections['@label'][-1]: - ln += ' \\' - script.append(ln) - - if '@entry' in sections: - if len(sections['@entry']) != 1: - raise TemplateSyntaxError("You must have one and only one entrypoint!") - else: - script.append('') - script.append(f"ENTRYPOINT {str(sections['@entry'][0].split())}") + @abstractmethod + def _arguments(self, all_contents: dict[str, list[str]]) -> list[str]: + """Handle arguments.""" - script.append('') + @abstractmethod + def _copy(self, contents: list[str]) -> list[str]: + """Handle the @copy section.""" - return script + @abstractmethod + def _run(self, contents: list[str], label_contents: list[str]) -> list[str]: + """Handle the @run section. If any !enver directives are found, return a list for the @env section.""" - def generate_build_cmd(self, src: str, dest: str, args: list = None) -> str: - arguments = ' ' + ' '.join( - _ for _ in args - ) if args is not None else '' - script = f' -f {src}' - destination = f' -t {dest}' - end = ' .;' + @abstractmethod + def _env(self, contents: list[str]) -> list[str]: + """Handle the @env section.""" - cmd = ['podman build', arguments, script, destination, end] - return ''.join(_ for _ in cmd) + @abstractmethod + def _label(self, contents: list[str]) -> list[str]: + """Handle the @label section.""" - def format_image_name(self, path: Path, tag: str) -> str: - return f"{'localhost/' if '/' not in tag else ''}{tag}{':latest' if ':' not in tag else ''}" + @abstractmethod + def _entry(self, contents: list[str]) -> list[str]: + """Handle the @entry section.""" - def clean_up_old_image_cmd(self, name: str) -> str: - return f'podman untag {name}' + @classmethod + def _literal_section(cls, contents: list[str]) -> list[str]: + """Handle literal sections.""" + ret: list = [""] + for ln in contents: + ret.append(ln.lstrip("|")) + return ret + @abstractmethod + def generate_build_cmd(self, src: str, dest: str, args: list[str] = None) -> str: + """Generate CLI command to build.""" -class Apptainer(Backend): + @abstractmethod + def format_image_name(self, path: Path, image: Image) -> str: + """Create a name for the image build.""" + + @abstractmethod + def clean_up_old_image_cmd(self, path: Path, name: str) -> str: + """Generate CLI command to clean up an old image.""" - def __init__(self, variables: dict): - super().__init__(name='apptainer', variables=variables) - def generate_script(self, file: Path, variables: dict) -> list: - script = list() - template = self._load_template(file, variables) - sections = self._get_sections(template) +class Podman(Backend): + """Podman backend.""" - if '@arg' in sections: - if len(sections['@arg']) > 0: - vbs = dict() - for a in sections['@arg']: - if len(a.split()) != 1: + def __init__(self): + super().__init__(name="podman", executable="podman") + + def _from(self, contents: list[str]) -> list[str]: + return [f"FROM {contents[0]}"] + + def _arguments(self, all_contents: dict[str, list[str]]) -> list[str]: + ret: list[str] = list() + for si in all_contents.keys(): + for li in range(len(all_contents[si])): + res: re_Match[str] = re_match(r".*(@@\s*(\S+)\s*@@).*", all_contents[si][li]) + if res is not None: + if len(res.group(2).split()) != 1: raise TemplateSyntaxError("Arguments cannot have spaces in their names!") else: - vbs[a] = '{{ ' + a + ' }}' - - for si in sections.keys(): - for li in range(len(sections[si])): - sections[si][li] = _substitute(sections[si][li], vbs, r'(? 0: + ret.insert(0, "") + return ret + + def _copy(self, contents: list[str]) -> list[str]: + ret: list[str] = [""] + for ln in contents: + if len(ln.split()) != 2: + raise TemplateSyntaxError("Entries in @copy can only have one source and destination!", ln) + ret.append(f"COPY {ln}") + return ret + + def _run(self, contents: list[str], label_contents: list[str]) -> list[str]: + ret: list[str] = [""] + for cmd in contents: + # process !envar directives + alt_cmd = cmd + if re_match(r"^!envar\s+.*", cmd): + res = re_match(r"^!envar\s+(?P\S+)\s+(?P.*)$", cmd) + alt_cmd = 'export {name}="{value}"'.format(**res.groupdict()) + label_contents.append("{name} {value}".format(**res.groupdict())) + # generate line + ln = "" + # place RUN on the first line + if cmd == contents[0]: + ln += "RUN " else: - raise TemplateSyntaxError("Unknown source format!", sections['@from'][0]) - script.append(f"From: {sections['@from'][0]}") - - if '@pre' in sections: - if len(sections['@pre']) > 0: - script.append('') - for ln in sections['@pre']: - script.append(ln.lstrip('|')) - - if '@copy' in sections: - if len(sections['@copy']) > 0: - script.append('') - script.append('%files') - for ln in sections['@copy']: - if len(ln.split()) != 2: - raise TemplateSyntaxError("Your '@copy' can only have one source and destination!", ln) - script.append(f" {ln}") - - if '@run' in sections: - if len(sections['@run']) > 0: - script.append('') - script.append('%post') - for cmd in sections['@run']: - script.append(f' {cmd}') - - if '@env' in sections: - if len(sections['@env']) > 0: - script.append('') - script.append('%environment') - for env in sections['@env']: - parts = env.split() - script.append(f" export {parts[0]}=\"{env.lstrip(parts[0]).strip(' ')}\"") - - if '@label' in sections: - if len(sections['@label']) > 0: - script.append('') - script.append('%labels') - for label in sections['@label']: - parts = label.split() - script.append(f" {parts[0]} {label.lstrip(parts[0]).strip(' ')}") - - if '@entry' in sections: - if len(sections['@entry']) != 1: - raise TemplateSyntaxError("You must have one and only one entrypoint!") + # indent following lines + ln += " " + ln += alt_cmd + # add '&& \\' to all but the last line + if cmd != contents[-1]: + ln += " && \\" + ret.append(ln) + return ret + + def _env(self, contents: list[str]) -> list[str]: + ret: list[str] = [""] + for env in contents: + parts = env.split() + # generate line + ln = "" + # add ENV to the first line + if env == contents[0]: + ln += "ENV " else: - script.append('') - script.append('%runscript') - script.append(f" {sections['@entry'][0]}") + # indent following lines + ln += " " + ln += f"{parts[0]}=\"{env.lstrip(parts[0]).strip(' ')}\"" + # add '\\' to all but the last line + if env != contents[-1]: + ln += " \\" + ret.append(ln) + return ret + + def _label(self, contents: list[str]) -> list[str]: + ret: list[str] = [""] + for label in contents: + parts = label.split() + if len(parts) != 2: + raise TemplateSyntaxError("Label '{}' must have two parts!".format(label)) + # generate line + ln = "" + # add LABEL to the first line + if label == contents[0]: + ln += "LABEL " + else: + # indent following lines + ln += " " + ln += f"{parts[0]}=\"{label.lstrip(parts[0]).strip(' ')}\"" + # add '\\' to all but the last line + if label != contents[-1]: + ln += " \\" + ret.append(ln) + return ret + + def _entry(self, contents: list[str]) -> list[str]: + return ["", "ENTRYPOINT {}".format(contents[0].split())] - if '@post' in sections: - if len(sections['@post']) > 0: - script.append('') - for ln in sections['@post']: - script.append(ln.lstrip('|')) + def generate_build_cmd(self, src: str, dest: str, args: list = None) -> str: + cmd: list[str] = ["{} build".format(self.executable)] + # arguments + if args is not None and len(args) > 0: + cmd.append(" ".join(_ for _ in args) if args is not None else "") + # script + cmd.append("-f {}".format(src)) + # destination + cmd.append("-t {}".format(dest)) + # build dir + cmd.append(".") + return " ".join(_ for _ in cmd) + ";" - script.append('') + def format_image_name(self, path: Path, tag: str) -> str: + return "{}{}{}".format("localhost/" if "/" not in tag else "", tag, ":latest" if ":" not in tag else "") - return script + def clean_up_old_image_cmd(self, path: Path, name: str) -> str: + return "podman untag {}".format(name) - def generate_build_cmd(self, src: str, dest: str, args: list = None) -> str: - arguments = ' ' + ' '.join( - x for x in args - ) if args is not None else '' - script = f' {src}' - destination = f' {dest}' - end = ';' - cmd = ['apptainer build', arguments, destination, script, end] - return ''.join(_ for _ in cmd) +class Apptainer(Backend): + """Apptainer backend.""" + + def __init__(self): + super().__init__(name="apptainer", executable="apptainer") + + def _from(self, contents: list[str]) -> list[str]: + ret: list[str] = list() + if re_match(r"^.*\.sif$", contents[0]): + ret.append("Bootstrap: localimage") + elif re_match(r"^.*\/.*:.*$", contents[0]): + ret.append("Bootstrap: docker") + else: + raise TemplateSyntaxError("Unknown source format in @from!", contents[0]) + ret.append("From: {}".format(contents[0])) + return ret + + def _arguments(self, all_contents: dict[str, list[str]]) -> list[str]: + for si in all_contents.keys(): + for li in range(len(all_contents[si])): + res = re_match(r".*(@@\s*(\S+)\s*@@).*", all_contents[si][li]) + if res is not None: + if len(res.group(2).split()) != 1: + raise TemplateSyntaxError("Arguments cannot have spaces in their names!") + else: + all_contents[si][li] = _substitute( + all_contents[si][li], + {res.group(2): f"{{{{ {res.group(2)} }}}}"}, + r"@@\s*(\S+)\s*@@", + ) + return list() + + def _copy(self, contents: list[str]) -> list[str]: + ret: list[str] = ["", "%files"] + for ln in contents: + if len(ln.split()) != 2: + raise TemplateSyntaxError("Your '@copy' can only have one source and destination!", ln) + ret.append(" {}".format(ln)) + return ret + + def _run(self, contents: list[str], label_contents: list[str]) -> list[str]: + ret: list[str] = ["", "%post"] + for cmd in contents: + # handel !envar directives + if re_match(r"^!envar\s+.*", cmd): + res = re_match(r"^!envar\s+(?P\S+)\s+(?P.*)$", cmd) + cmd = 'export {name}="{value}"'.format(**res.groupdict()) + label_contents.append("{name} {value}".format(**res.groupdict())) + ret.append(" {}".format(cmd)) + return ret + + def _env(self, contents: list[str]) -> list[str]: + ret: list[str] = ["", "%environment"] + for env in contents: + parts = env.split() + ret.append(' export {}="{}"'.format(parts[0], env.lstrip(parts[0]).strip(" "))) + return ret + + def _label(self, contents: list[str]) -> list[str]: + ret: list[str] = ["", "%labels"] + for label in contents: + parts = label.split() + ret.append(" {} {}".format(parts[0], label.lstrip(parts[0]).strip(" "))) + return ret + + def _entry(self, contents: list[str]) -> list[str]: + return ["", "%runscript", " {}".format(contents[0])] + + def generate_build_cmd(self, src: str, dest: str, args: list = None) -> str: + cmd: list[str] = ["{} build".format(self.executable)] + # arguments + if args is not None and len(args) > 0: + cmd.append(" ".join(_ for _ in args) if args is not None else "") + # script + cmd.append("{}".format(src)) + # destination + cmd.append("{}".format(dest)) + return " ".join(_ for _ in cmd) + ";" def format_image_name(self, path: Path, tag: str) -> str: - return f"{Path.joinpath(path, tag)}{'.sif' if '.sif' not in tag else ''}" + return "{}{}".format(Path.joinpath(path, tag), ".sif" if ".sif" not in tag else "") - def clean_up_old_image_cmd(self, name: str) -> str: - return 'echo' + def clean_up_old_image_cmd(self, path: Path, name: str) -> str: + # TODO we need to think about apptainer image caching + return "echo" -def get_backend(variables: dict) -> Backend: +def get_backend() -> Backend: backend = config.get("velocity:backend") - if backend == 'podman': - return Podman(variables) - elif backend == 'apptainer': - return Apptainer(variables) + if backend == "podman": + b = Podman() + elif backend == "apptainer": + b = Apptainer() else: raise BackendNotSupported(backend) + + # check that backend is available + if b.is_available(): + return b + else: + raise BackendNotAvailable("Your system does not have the '{}' backend".format(b.name)) diff --git a/src/velocity/_config.py b/src/velocity/_config.py index 502b662..09887cf 100644 --- a/src/velocity/_config.py +++ b/src/velocity/_config.py @@ -1,212 +1,32 @@ +"""Config organizer. Provides default config object for velocity.""" + from loguru import logger -from ._exceptions import InvalidSchema, InvalidConfig +from ._exceptions import InvalidConfigIdentifier from platform import processor as arch from pathlib import Path -from os import getlogin as os_getlogin - -""" -Allowed schema configurations: - -bool: - required: bool=True - default: bool - -str: - required: bool=True - default: str - allowed: list[str]=None # None is treated as if there are no restrictions - -list: - required: bool=True - default: list - items: {schema} # the types of items in the list (bool or str) - -dict: - required: bool=True - auto: bool=False # if not defined auto-generate - cardinality: str="one" # allowed values are 'one' or 'many' - # If many, the dict name is ignored and n matches of the dict are allowed - properties: dict -""" - -DEFAULT_BOOL_SCHEMA = { - "type": bool, -} - -DEFAULT_STR_SCHEMA = { - "type": str, -} - -DEFAULT_LIST_SCHEMA = { - "type": list, - "default": list(), - "items": { - "type": str, - "required": False - } -} - -IM_SCHEMA = { - "type": str, - "default": "COMBINE", - "allowed": ["COMBINE", "OVERRIDE"] -} - -VELOCITY_SCHEMA = { - "type": dict, - "properties": { - "velocity": { - "type": dict, - "properties": { - "backend": DEFAULT_STR_SCHEMA, - "distro:": DEFAULT_STR_SCHEMA, - "system": DEFAULT_STR_SCHEMA, - "verbose": { - "type": bool, - "default": False - }, - "debug": { - "type": str, - "default": "INFO", - "allowed": ["INFO", "DEBUG"] - } - } - }, - "build_specs": { - "type": dict, - "default": { - "type": dict, - "auto": True, - "properties": { - "variables": DEFAULT_LIST_SCHEMA - } - }, - "system": { - "type": dict, - "required": False, - "cardinality": "many", - "properties": { - "default": { - "type": dict, - "auto": True, - "properties": { - "prolog_lines": DEFAULT_LIST_SCHEMA, - "IM_prolog_lines": IM_SCHEMA, - "variables": DEFAULT_LIST_SCHEMA, - "IM_variables": IM_SCHEMA - } - }, - "backend": { - "type": dict, - "required": False, - "cardinality": "many", - "properties": { - "default": { - "type": dict, - "auto": True, - "properties": { - "arguments": DEFAULT_LIST_SCHEMA, - "IM_arguments": IM_SCHEMA, - "prolog_lines": DEFAULT_LIST_SCHEMA, - "IM_prolog_lines": IM_SCHEMA, - "variables": DEFAULT_LIST_SCHEMA, - "IM_variables": IM_SCHEMA - } - }, - "image": { - "type": dict, - "required": False, - "cardinality": "many", - "properties": { - "arguments": DEFAULT_LIST_SCHEMA, - "IM_arguments": IM_SCHEMA, - "dependencies": DEFAULT_LIST_SCHEMA, - "IM_dependencies": IM_SCHEMA, - "prolog_script": { - "type": str, - "default": None - }, - "variables": DEFAULT_LIST_SCHEMA, - "IM_variables": IM_SCHEMA - } - } - } - } - } - } - } - } -} - - -def validate_and_generate_config(config: dict | list | str | bool | None, schema: dict) -> bool: - try: - if schema["type"] == dict: - allowed = {"type", "required", "auto", "cardinality", "properties"} - if not allowed.issuperset(schema.keys()): - raise InvalidSchema("schema {} contains invalid attributes: {}".format( - schema, - set(schema).difference(allowed) - )) - if not isinstance(schema["type"], type) or \ - not isinstance(schema["required"], bool) or \ - not isinstance(schema["auto"], bool) or \ - schema["cardinality"] not in ("one", "many") or \ - not (isinstance(schema["properties"], dict) and schema["properties"] is not None): - raise InvalidSchema("schema {} contains invalid attribute types".format( - schema - )) - if not isinstance(config, dict) and config is not None: - raise InvalidConfig("config type '{}' does not match schema type '{}'".format( - type(config), - schema["type"] - )) - - if schema["required"]: - if config is None: - raise InvalidConfig("schema required but config is '{}'".format( - None - )) - else: - for p in config.keys(): - for s in schema["properties"]: - validate_and_generate_config(config[p], s) - else: - if config is None: - return True - - - # return True - elif schema["type"] == list: - pass - elif schema["type"] == str: - pass - elif schema["type"] == bool: - pass - else: - raise InvalidSchema("schema type '{}' is not valid".format(schema["type"])) - - except (KeyError, TypeError): - logger.warning("schema {} is not a dict or does not specify type".format(schema)) - except (InvalidSchema, InvalidConfig) as e: - logger.warning(e) - return False +from os import getlogin as get_username class Config: - """Configuration class.""" + """Configuration class. Stores configuration as a dictionary.""" + def __init__(self) -> None: self._config = dict() def set(self, item: str, value: int | bool | str | list | dict | None) -> None: - """Set config property.""" + """Set configuration property.""" try: + # do not let user set the root node if item != "": parts: list[str] = item.split(":") set_value = self._config for p in parts: + # make all config identifiers comply with python identifiers if not p.isidentifier(): - logger.warning("'{}' is not a valid identifier", format(item)) + raise InvalidConfigIdentifier( + "'{}' is not a valid identifier.".format(item) + ) + # walk config tree until the final node is found if p != parts[-1]: if p not in set_value: set_value[p] = dict() @@ -214,44 +34,49 @@ def set(self, item: str, value: int | bool | str | list | dict | None) -> None: else: set_value[p] = value else: - raise AttributeError("you cannot set the base config node") - except AttributeError as e: - logger.warning(e) + raise AttributeError("You cannot set the root config node.") + except (AttributeError, InvalidConfigIdentifier) as e: + logger.exception(e) def get(self, item: str) -> int | bool | str | list | dict | None: - """Get config property.""" + """Get configuration property. Return None if not found""" try: if item != "": parts: list[str] = item.split(":") ret_value = self._config + # make all config identifiers comply with python identifiers for p in parts: if not p.isidentifier(): - logger.warning("'{}' is not a valid identifier", format(item)) - return None + raise InvalidConfigIdentifier( + "'{}' is not a valid identifier.".format(item) + ) ret_value = ret_value[p] return ret_value else: return self._config except (KeyError, TypeError): - logger.warning("could not find '{}' in config", format(item)) + logger.warning("Could not find '{}' in config.", format(item)) + except InvalidConfigIdentifier as e: + logger.exception(e) return None def __str__(self) -> str: return str(self._config) -# default configuration +# default configuration & singleton _config = Config() -_config.set("velocity", { - "system": arch(), - "backend": "apptainer", - "distro": "ubuntu", - "verbose": False, - "debug": "WARNING", - "config_dir": Path.home().joinpath(".velocity"), - "build_dir": Path("/tmp").joinpath(os_getlogin(), "velocity") -}) - - -# config singleton +_config.set( + "velocity", + { + "system": arch(), + "backend": "apptainer", + "distro": "ubuntu", + "verbose": False, + "debug": "WARNING", + "config_dir": Path.home().joinpath(".velocity", "config"), + "image_dir": Path.home().joinpath(".velocity", "images"), + "build_dir": Path("/tmp").joinpath(get_username(), "velocity"), + }, +) config = _config diff --git a/src/velocity/_graph_old.py b/src/velocity/_graph_old.py deleted file mode 100644 index f3eadeb..0000000 --- a/src/velocity/_graph_old.py +++ /dev/null @@ -1,519 +0,0 @@ -import re -import yaml -import networkx as nx -from enum import Enum -from pathlib import Path -from ._exceptions import EdgeViolatesDAG, InvalidDependencySpecification, NoAvailableBuild - - -def get_permutations(idx: int, sets: list[list]): - """ - Get all the possible permutations from a list of lists - :param idx: recursion level - :param sets: sets for permutation - :return: permutations - """ - permutations = list() - if len(sets) == 0: - pass - elif idx == len(sets) - 1: - for i in sets[idx]: - permutations.append({i}) - else: - for i in sets[idx]: - sub_permutations = get_permutations(idx + 1, sets) - for sub in sub_permutations: - permutations.append(sub.union({i})) - return permutations - - -class Tag: - - def __init__(self, tag_s: str): - self.tag = tag_s - self.tag_parts = re.split(r'\.|-', tag_s) - - def __eq__(self, other) -> bool: - if isinstance(other, Tag) and self.tag_parts == other.tag_parts: - return True - else: - return False - - def __ne__(self, other) -> bool: - if not isinstance(other, Tag) or self.tag_parts != other.tag_parts: - return True - else: - return False - - def __gt__(self, other) -> bool: - if not isinstance(other, Tag): - raise TypeError(f"'>' not supported between instances of " - f"'{type(self).__name__}' and '{type(other).__name__}'") - else: - comparable_parts = min(len(self.tag_parts), len(other.tag_parts)) - for idx in range(comparable_parts): - if self.tag_parts[idx].isdecimal() and other.tag_parts[idx].isdecimal(): - if int(self.tag_parts[idx]) > int(other.tag_parts[idx]): - return True - elif int(self.tag_parts[idx]) == int(other.tag_parts[idx]): - continue - else: - return False - else: - if self.tag_parts[idx] > other.tag_parts[idx]: - return True - elif self.tag_parts[idx] == other.tag_parts[idx]: - continue - else: - return False - - # if we got here it means that their comparable parts are equal, so we compare on length. - if len(self.tag_parts) > len(other.tag_parts): - return True - else: - return False - - def __ge__(self, other) -> bool: - if not isinstance(other, Tag): - raise TypeError(f"'>=' not supported between instances of " - f"'{type(self).__name__}' and '{type(other).__name__}'") - else: - comparable_parts = min(len(self.tag_parts), len(other.tag_parts)) - for idx in range(comparable_parts): - if self.tag_parts[idx].isdecimal() and other.tag_parts[idx].isdecimal(): - if int(self.tag_parts[idx]) > int(other.tag_parts[idx]): - return True - elif int(self.tag_parts[idx]) == int(other.tag_parts[idx]): - continue - else: - return False - else: - if self.tag_parts[idx] > other.tag_parts[idx]: - return True - elif self.tag_parts[idx] == other.tag_parts[idx]: - continue - else: - return False - - # if we got here it means that their comparable parts are equal, so we compare on length. - if len(self.tag_parts) >= len(other.tag_parts): - return True - else: - return False - - def __lt__(self, other) -> bool: - if not isinstance(other, Tag): - raise TypeError(f"'<' not supported between instances of " - f"'{type(self).__name__}' and '{type(other).__name__}'") - else: - comparable_parts = min(len(self.tag_parts), len(other.tag_parts)) - for idx in range(comparable_parts): - if self.tag_parts[idx].isdecimal() and other.tag_parts[idx].isdecimal(): - if int(self.tag_parts[idx]) < int(other.tag_parts[idx]): - return True - elif int(self.tag_parts[idx]) == int(other.tag_parts[idx]): - continue - else: - return False - else: - if self.tag_parts[idx] < other.tag_parts[idx]: - return True - elif self.tag_parts[idx] == other.tag_parts[idx]: - continue - else: - return False - - # if we got here it means that their comparable parts are equal, so we compare on length. - if len(self.tag_parts) < len(other.tag_parts): - return True - else: - return False - - def __le__(self, other) -> bool: - if not isinstance(other, Tag): - raise TypeError(f"'<=' not supported between instances of " - f"'{type(self).__name__}' and '{type(other).__name__}'") - else: - comparable_parts = min(len(self.tag_parts), len(other.tag_parts)) - for idx in range(comparable_parts): - if self.tag_parts[idx].isdecimal() and other.tag_parts[idx].isdecimal(): - if int(self.tag_parts[idx]) < int(other.tag_parts[idx]): - return True - elif int(self.tag_parts[idx]) == int(other.tag_parts[idx]): - continue - else: - return False - else: - if self.tag_parts[idx] < other.tag_parts[idx]: - return True - elif self.tag_parts[idx] == other.tag_parts[idx]: - continue - else: - return False - - # if we got here it means that their comparable parts are equal, so we compare on length. - if len(self.tag_parts) <= len(other.tag_parts): - return True - else: - return False - - def __id__(self): - """ - Alt form of __str__ - """ - return self.tag_parts.__str__() - - def __str__(self): - return self.tag - - -class Node: - """ - Node class for an image dependency graph. Uniquely identified by image name, - host system, base image distro and image tag. - """ - - def __init__(self, name: str, tag: str, path: Path = None, build_specifications: dict = None, - specifications_yaml: dict = None) -> None: - self.name = name - self.tag = Tag(tag) - self.path = path - self.build_specifications = build_specifications - self.specifications_yaml = specifications_yaml - - def similar(self, other) -> bool: - """ - Check if two nodes share the same name. - """ - if isinstance(other, Node) and self.name == other.name: - return True - else: - return False - - def __eq__(self, other) -> bool: - if isinstance(other, Node) and self.name == other.name and self.tag == other.tag: - return True - else: - return False - - def __ne__(self, other) -> bool: - if not isinstance(other, Node) or self.name != other.name or self.tag != other.tag: - return True - else: - return False - - def __gt__(self, other) -> bool: - if not isinstance(other, Node): - raise TypeError(f"'>' not supported between instances of " - f"'{type(self).__name__}' and '{type(other).__name__}'") - elif self.name > other.name: - return True - elif self.name == other.name and self.tag > other.tag: - return True - else: - return False - - def __ge__(self, other) -> bool: - if not isinstance(other, Node): - raise TypeError(f"'>=' not supported between instances of " - f"'{type(self).__name__}' and '{type(other).__name__}'") - elif self.name > other.name: - return True - elif self.name == other.name and self.tag >= other.tag: - return True - else: - return False - - def __lt__(self, other) -> bool: - if not isinstance(other, Node): - raise TypeError(f"'<' not supported between instances of " - f"'{type(self).__name__}' and '{type(other).__name__}'") - elif self.name < other.name: - return True - elif self.name == other.name and self.tag < other.tag: - return True - else: - return False - - def __le__(self, other) -> bool: - if not isinstance(other, Node): - raise TypeError(f"'<=' not supported between instances of " - f"'{type(self).__name__}' and '{type(other).__name__}'") - elif self.name < other.name: - return True - elif self.name == other.name and self.tag <= other.tag: - return True - else: - return False - - def __hash__(self) -> int: - return hash(self.name + self.tag.__id__()) - - def __str__(self) -> str: - return f'Node({self.name}, {self.tag})' - - -class DepOp(Enum): - """ - Specify dependency options - """ - EQ = '=' - GE = '^' - LE = '_' - UN = None - - -class Target: - """ - Build specification targets - """ - - def __init__(self, node: Node, op: DepOp): - self.node = node - self.op = op - - def __str__(self): - return f'Target: {self.op} -> {str(self.node)}' - - -class ImageGraph(nx.DiGraph): - - def __init__(self, path: str, backend: str, system: str, distro: str, **attr) -> None: - super().__init__(**attr) - self.backend = backend - self.system = system - self.distro = distro - - p = Path(path) - if not p.is_dir(): - raise NotADirectoryError(f"Image path {path} is not a directory!") - - # add nodes to graph (dependencies are added later once all nodes are in the graph) - for name in [x for x in p.iterdir() if x.is_dir() and x.name[0] != '.']: - for tag in [x for x in name.iterdir() if x.is_dir()]: - with open(Path.joinpath(tag, 'specifications.yaml'), 'r') as file: - specifications = yaml.safe_load(file) - if specifications is None: - raise TypeError("Specification can not be None") - if self.system in specifications['build_specifications']: - if self.backend in specifications['build_specifications'][self.system]: - if self.distro in specifications['build_specifications'][self.system][self.backend]: - self.add_node(Node(name.name, tag.name, tag.absolute(), - specifications['build_specifications'] - [self.system][self.backend][self.distro], specifications)) - - # add dependency edges to graph - for node in self.nodes: - if 'dependencies' in node.build_specifications: - for dependency in node.build_specifications['dependencies']: - if dependency is None: - raise InvalidDependencySpecification(dependency, node.name, node.tag, - Path.joinpath(node.path, 'specifications.yaml')) - else: - # split up specification into parts - result = re.search(r'^(.*)@(.*)([%^_=])(.*)$', dependency) - - # add the specified edge(s) to the graph - dependency_fulfilled = False - if result is not None: - if result[3] == '=': - self.add_edge(node, Node(result[1], result[4])) - dependency_fulfilled = True - elif result[3] == '^': - for n in self.nodes: - if n.similar(Node(result[1], '')) and n >= Node(result[1], result[4]): - self.add_edge(node, n) - dependency_fulfilled = True - elif result[3] == '_': - for n in self.nodes: - if n.similar(Node(result[1], '')) and n <= Node(result[1], result[4]): - self.add_edge(node, n) - dependency_fulfilled = True - elif result[3] == '%': - for n in self.nodes: - if Node(result[1], result[2]) <= n <= Node(result[1], result[4]): - self.add_edge(node, n) - dependency_fulfilled = True - else: - for n in self.nodes: - if n.similar(Node(dependency, '')): - self.add_edge(node, n) - dependency_fulfilled = True - # check that the dependency was fulfilled - if not dependency_fulfilled: - raise InvalidDependencySpecification(dependency, node.name, node.tag, - Path.joinpath(node.path, 'specifications.yaml')) - - def add_edge(self, u_of_edge: Node, v_of_edge: Node, **attr): - # check that edge endpoint are in graph - if self.has_node(u_of_edge) and self.has_node(v_of_edge): - super().add_edge(u_of_edge, v_of_edge, **attr) - else: - raise InvalidDependencySpecification(f'{v_of_edge.name}@={v_of_edge.tag}', - u_of_edge.name, u_of_edge.tag, - Path.joinpath(u_of_edge.path, 'specifications.yaml')) - - # check that graph is still a DAG - if not nx.is_directed_acyclic_graph(self): - cycle = nx.find_cycle(self) - raise EdgeViolatesDAG(u_of_edge, v_of_edge, cycle) - - def get_similar_nodes(self, node: Node) -> set: - similar = set() - for n in self.nodes: - if n.similar(node): - similar.add(n) - return similar - - def get_dependencies(self, node: Node): - # had to add this wrapper because nx.neighbors was dropping the attributes of some nodes (cuda, python) - deps = set() - for node in nx.neighbors(self, node): - for n in self.nodes: - if n == node: - deps.add(n) - # will contact the maintainers of networkx at some point - - return deps - - def is_above(self, u_node: Node, v_node: Node) -> bool: - """ - Test if one node is above another in the dependency tree - """ - return nx.has_path(self, u_node, v_node) - - def _is_valid_build_tuple(self, bt: tuple[Node]) -> bool: - """ - Verify that all the dependencies of a build tuple can be met. - """ - valid = True - - # check for similar images - for i0 in range(len(bt)): - for i2 in bt[i0 + 1:]: - if bt[i0].similar(i2): - valid = False - - # check that all images exist - for node in bt: - if not self.has_node(node): - valid = False - - # break prematurely if the first two checks fail - if not valid: - return valid - - # check that deps in build tuple - for node in bt: - deps = self.get_dependencies(node) - - # group deps - grouped = dict() - for d in deps: - if d.name not in grouped: - grouped[d.name] = set() - grouped[d.name].add(d) - - # check that the needed dependency exists - for g in grouped: - if grouped[g].isdisjoint(bt): - valid = False - - return valid - - def create_build_recipe(self, targets: list[Target]) -> tuple: - - # check if all the targets exist - for node in targets: - if len(self.get_similar_nodes(node.node)) < 1: - raise NoAvailableBuild(f"The build target {node.node} does not exist!") - - # init build set and priority list - build_set = set() - priority_list = list() - - # add similar to build set - for target in targets: - build_set.update(self.get_similar_nodes(target.node)) - # make sure to update priority - if target.node.name not in priority_list: - priority_list.append(target.node.name) - - # add deps to build set - while True: - build_set_length = len(build_set) - - for node in build_set.copy(): - dependencies = self.get_dependencies(node) - for d in dependencies: - build_set.add(d) - if d.name not in priority_list: - priority_list.append(d.name) - - # loop until all dependencies are added - if build_set_length == len(build_set): - break - - # apply constraints - for target in targets: - for node in build_set.copy(): - if node.similar(target.node): - if target.op == DepOp.EQ and node != target.node: - build_set.remove(node) - elif target.op == DepOp.GE and node < target.node: - build_set.remove(node) - elif target.op == DepOp.LE and node > target.node: - build_set.remove(node) - - # group deps - grouped = dict() - for node in build_set: - if node.name not in grouped: - grouped[node.name] = set() - grouped[node.name].add(node) - # sort deps so that the highest versions of images further up the dep tree will be chosen - prioritized_list_group = list() - for group in priority_list: - tmp = list(grouped[group]) - tmp.sort(reverse=True) - prioritized_list_group.append(tmp) - # get permutations - permutations = get_permutations(0, prioritized_list_group) - - # return valid build tuple - for p in permutations: - - # clean up permutation - clean_p = set() - for n in p: - for t in targets: - con_targ = None - for nt in [x for x in p]: - if nt.similar(t.node): - con_targ = nt - break - if self.is_above(con_targ, n): - clean_p.add(n) - break - - if self._is_valid_build_tuple(tuple(clean_p)): - # order build - build_list = list() - processed = set() - unprocessed = clean_p.copy() - while len(unprocessed) > 0: - level_holder = list() - for node in unprocessed.copy(): - deps = set(self.get_dependencies(node)).intersection(clean_p) - if deps.issubset(processed): - level_holder.append(node) - level_holder.sort() - for node in level_holder: - processed.add(node) - unprocessed.remove(node) - build_list.append(node) - - return tuple(build_list) - - # if we got here no valid build tuple could be found - raise NoAvailableBuild("No Available build!") diff --git a/src/velocity/_print.py b/src/velocity/_print.py index 9359078..086b70d 100644 --- a/src/velocity/_print.py +++ b/src/velocity/_print.py @@ -1,40 +1,50 @@ +"""Printing formatter.""" + from colorama import Fore, Back, Style class TextBlock: - - def __init__(self, text: str, fore: Fore = Fore.RESET, back: Back = Back.RESET, - style: Style = Style.NORMAL) -> None: + """Defines a block of text and its styling.""" + + def __init__( + self, + text: str, + fore: Fore = Fore.RESET, + back: Back = Back.RESET, + style: Style = Style.NORMAL, + ) -> None: self.text = text self.fore = fore self.back = back self.style = style -def p1print(tb: list[TextBlock]) -> None: - text_blocks = [ - TextBlock('==> ', fore=Fore.GREEN, style=Style.BRIGHT) - ] - text_blocks.extend(tb) - print_text_blocks(text_blocks) +def bare_print(tb: list[TextBlock]) -> None: + """Print a list of TextBlocks.""" + for text_block in tb: + print( + "{}{}{}{}{}".format( + text_block.fore, + text_block.back, + text_block.style, + text_block.text, + Style.RESET_ALL, + ), + end="", + ) + # print a newline at the end + print() -def sp1print(tb: list[TextBlock]) -> None: - text_blocks = [ - TextBlock('\t', fore=Fore.GREEN, style=Style.BRIGHT) - ] +def header_print(tb: list[TextBlock]) -> None: + """Print a list of TextBlocks with a preceding '==> ' block.""" + text_blocks = [TextBlock("==> ", fore=Fore.GREEN, style=Style.BRIGHT)] text_blocks.extend(tb) - print_text_blocks(text_blocks) + bare_print(text_blocks) -def wprint(tb: list[TextBlock]) -> None: - for b in tb: - b.fore = Fore.RED - b.style = Style.BRIGHT - print_text_blocks(tb) - - -def print_text_blocks(text_blocks: list[TextBlock]): - for text_block in text_blocks: - print(f'{text_block.fore}{text_block.back}{text_block.style}{text_block.text}{Style.RESET_ALL}', end='') - print() +def indent_print(tb: list[TextBlock]) -> None: + """Print a list of TextBlocks with a preceding 4 spaces.""" + text_blocks = [TextBlock(" ", fore=Fore.GREEN, style=Style.BRIGHT)] + text_blocks.extend(tb) + bare_print(text_blocks) diff --git a/tests/test__config.py b/tests/test__config.py index 9ce92ee..d7ed05c 100644 --- a/tests/test__config.py +++ b/tests/test__config.py @@ -1,59 +1,39 @@ from unittest import TestCase -from src.velocity._config import validate_and_generate_config - -TEST_SCHEMA = { - "type": dict, - "properties": { - "test_dict": { - "type": dict, - "properties": { - "test_list": {}, - "test_str": {}, - "test_bool": {} - } - }, - "missing_dict": {} +from src.velocity._config import Config + +c = Config() +c._config = { + "test_one": 7, + "test_two": False, + "test_three": [5, "a"], + "test_four": { + "inner_test": "test" } } - -class Test(TestCase): - """ - def test_validate_config(self): - # misc - self.assertFalse(validate_and_generate_config({}, {})) - self.assertFalse(validate_and_generate_config(1, {})) - - # dict - ts = { - "type": dict, - "required": True, - "auto": False, - "cardinality": "one", - "properties": {} - } - self.assertTrue(validate_and_generate_config({}, ts)) - self.assertFalse(validate_and_generate_config(None, ts)) - self.assertFalse(validate_and_generate_config({"test": 0}, ts)) - - ts = { - "type": dict, - "required": True, - "auto": False, - "cardinality": "one", - "properties": { - "test": { - "type": bool +class TestConfig(TestCase): + def test_set(self): + c.set("test_four:inner_test", True) + c.set("one:two:three", "test") + c.set("again", 56) + t = { + "again": 56, + "test_one": 7, + "test_two": False, + "test_three": [5, "a"], + "test_four": { + "inner_test": True + }, + "one": { + "two": { + "three": "test" } } } - self.assertFalse(validate_and_generate_config({"test": True}, ts)) - - # list - - # str - - # bool + self.assertEqual(t, c._config) - self.fail() - """ + def test_get(self): + self.assertEqual(7, c.get("test_one")) + self.assertFalse(c.get("test_two")) + self.assertEqual("a", c.get("test_three")[1]) + self.assertEqual("test", c.get("test_four:inner_test")) From f4d9a399b80f36912da0f5ba692aeedf534a071c Mon Sep 17 00:00:00 2001 From: Asa Rentschler Date: Fri, 6 Sep 2024 17:07:55 -0400 Subject: [PATCH 03/10] Refactored build --- pyproject.toml | 6 +- src/velocity/__init__.py | 4 - src/velocity/_backends.py | 24 +++- src/velocity/_build.py | 290 ++++++++++++++++++-------------------- src/velocity/_config.py | 6 +- 5 files changed, 162 insertions(+), 168 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e077167..4727370 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "velocity" -dynamic = [ "version" ] +version = "0.1.0-rc1" authors = [ { name="Asa"} ] @@ -15,9 +15,7 @@ dependencies = [ "networkx", "colorama", "editor", - "loguru", - "sys", - "shutil" + "loguru" ] [project.urls] diff --git a/src/velocity/__init__.py b/src/velocity/__init__.py index 2da6386..990b15e 100644 --- a/src/velocity/__init__.py +++ b/src/velocity/__init__.py @@ -1,9 +1,5 @@ from ._config import config -__version__ = "0.0.0" - -config.set("velocity:version", __version__) - __all__ = [ config ] diff --git a/src/velocity/_backends.py b/src/velocity/_backends.py index 61c2c5c..8a71932 100644 --- a/src/velocity/_backends.py +++ b/src/velocity/_backends.py @@ -217,13 +217,16 @@ def generate_build_cmd(self, src: str, dest: str, args: list[str] = None) -> str """Generate CLI command to build.""" @abstractmethod - def format_image_name(self, path: Path, image: Image) -> str: + def format_image_name(self, path: Path, tag: str) -> str: """Create a name for the image build.""" @abstractmethod - def clean_up_old_image_cmd(self, path: Path, name: str) -> str: + def clean_up_old_image_tag(self, name: str) -> str: """Generate CLI command to clean up an old image.""" + def build_exists(self, name: str) -> bool: + """Check if an images has been built.""" + class Podman(Backend): """Podman backend.""" @@ -344,9 +347,12 @@ def generate_build_cmd(self, src: str, dest: str, args: list = None) -> str: def format_image_name(self, path: Path, tag: str) -> str: return "{}{}{}".format("localhost/" if "/" not in tag else "", tag, ":latest" if ":" not in tag else "") - def clean_up_old_image_cmd(self, path: Path, name: str) -> str: + def clean_up_old_image_tag(self, name: str) -> str: return "podman untag {}".format(name) + def build_exists(self, name: str) -> bool: + return False + class Apptainer(Backend): """Apptainer backend.""" @@ -421,19 +427,23 @@ def generate_build_cmd(self, src: str, dest: str, args: list = None) -> str: # arguments if args is not None and len(args) > 0: cmd.append(" ".join(_ for _ in args) if args is not None else "") - # script - cmd.append("{}".format(src)) # destination cmd.append("{}".format(dest)) + # script + cmd.append("{}".format(src)) return " ".join(_ for _ in cmd) + ";" def format_image_name(self, path: Path, tag: str) -> str: return "{}{}".format(Path.joinpath(path, tag), ".sif" if ".sif" not in tag else "") - def clean_up_old_image_cmd(self, path: Path, name: str) -> str: - # TODO we need to think about apptainer image caching + def clean_up_old_image_tag(self, name: str) -> str: return "echo" + def build_exists(self, name: str) -> bool: + if Path(name).is_file(): + return True + return False + def get_backend() -> Backend: backend = config.get("velocity:backend") diff --git a/src/velocity/_build.py b/src/velocity/_build.py index f84381d..458d114 100644 --- a/src/velocity/_build.py +++ b/src/velocity/_build.py @@ -1,70 +1,55 @@ +"""Build velocity images.""" + import datetime -import random -import re import shutil import os -import string from timeit import default_timer as timer from subprocess import Popen, PIPE from queue import SimpleQueue from threading import Thread from pathlib import Path from colorama import Fore, Style - from ._config import config from ._graph import Image -from ._print import p1print, sp1print, TextBlock -from ._backends import get_backend +from ._print import header_print, indent_print, TextBlock +from ._backends import get_backend, Backend def read_pipe(pipe: PIPE, topic: SimpleQueue, prefix: str, log: SimpleQueue) -> None: - """ - Read a subprocess PIPE and place lines on topic queue and log queue - """ + """Read a subprocess PIPE and place lines on topic queue and log queue.""" while True: ln = pipe.readline() - if ln == '': + if ln == "": break else: - topic.put(ln.strip('\n')) - log.put("{} {}".format(prefix, ln.strip('\n'))) + topic.put(ln.strip("\n")) + log.put("{} {}".format(prefix, ln.strip("\n"))) def run(cmd: str, log_file: Path = None, verbose: bool = False) -> None: - """ - Run a system command logging all output and print if verbose. - """ - + """Run a system command logging all output to a file and print if verbose.""" # open log file (set to False if none is provided) - file = open(log_file, 'w') if log_file is not None else False + file = open(log_file, "w") if log_file is not None else False log = SimpleQueue() stdout = SimpleQueue() stderr = SimpleQueue() - process = Popen( - cmd, - shell=True, - stdout=PIPE, - stderr=PIPE, - universal_newlines=True - ) + process = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE, universal_newlines=True) - out = Thread(target=read_pipe, args=(process.stdout, stdout, 'STDOUT:', log)) - err = Thread(target=read_pipe, args=(process.stderr, stderr, 'STDERR:', log)) + out = Thread(target=read_pipe, args=(process.stdout, stdout, "STDOUT:", log)) + err = Thread(target=read_pipe, args=(process.stderr, stderr, "STDERR:", log)) out.start() err.start() # loop for real time output while process.poll() is None: - if not stdout.empty() and verbose: - sp1print([ - TextBlock(stdout.get(), fore=Fore.GREEN, style=Style.DIM) - ]) - if not log.empty() and file: - file.write(log.get() + '\n') - file.flush() + if verbose and not stdout.empty(): + indent_print([TextBlock(stdout.get(), fore=Fore.GREEN, style=Style.DIM)]) + if file and not log.empty(): + file.write(log.get() + "\n") + file.flush() # TODO is this needed? out.join() err.join() @@ -72,203 +57,206 @@ def run(cmd: str, log_file: Path = None, verbose: bool = False) -> None: # clear stdout & log if verbose: while not stdout.empty(): - sp1print([ - TextBlock(stdout.get(), fore=Fore.GREEN, style=Style.DIM) - ]) + indent_print([TextBlock(stdout.get(), fore=Fore.GREEN, style=Style.DIM)]) if file: while not log.empty(): - file.write(log.get() + '\n') - file.flush() + file.write(log.get() + "\n") + file.flush() # TODO is this needed? file.close() # if an error was encountered exit with the subprocess exit code if process.poll() != 0: while stderr.qsize(): - ln = stderr.get() - if verbose: - sp1print([ - TextBlock(ln, fore=Fore.RED, style=Style.DIM) - ]) + indent_print([TextBlock(stderr.get(), fore=Fore.RED, style=Style.DIM)]) exit(process.poll()) -""" -class BuildUnit: - def __init__(self, node: Image): - self.node = node - self.build_id = ''.join(random.choice(string.ascii_lowercase) for _ in range(8)) -""" +class ImageBuilder: + """Image building class.""" -class Builder: + def __init__( + self, + bt: tuple[Image], + build_name: str = None, + dry_run: bool = False, + remove_tags: bool = True, + clean_build_dir: bool = False, + verbose: bool = False, + ) -> None: + self.build_units: tuple[Image] = bt + self.build_name: str = build_name + self.dry_run: bool = dry_run + self.remove_tags: bool = remove_tags + self.clean_build_dir: bool = clean_build_dir + self.verbose: bool = verbose - def __init__(self, bt: tuple[Image], build_name: str = None, dry_run: bool = False, - leave_tags: bool = True, verbose: bool = False): - self.build_units = list() - self.build_name = build_name - self.dry_run = dry_run - self.leave_tags = leave_tags - self.verbose = verbose - - self.backend_engine = get_backend({}) + self.backend_engine: Backend = get_backend() # create build_dir if it does not exist self.build_dir = Path(config.get("velocity:build_dir")) self.build_dir.mkdir(mode=0o777, parents=True, exist_ok=True) - self.build_units = bt - - def build(self): + def build(self) -> None: + """Launch image builds.""" # store pwd pwd = Path(Path().absolute()) - - """# clear build_dir - for entry in self.build_dir.iterdir(): - if entry.is_dir(): - shutil.rmtree(entry) - else: - entry.unlink() - """ + # clean build_dir + if self.clean_build_dir: + for entry in self.build_dir.iterdir(): + if entry.is_dir(): + shutil.rmtree(entry) + else: + entry.unlink() last = None # last image that was built for u in self.build_units: - if u == self.build_units[-1]: - sorted_specs = [(bu.name, bu.version) for bu in self.build_units] - sorted_specs.sort() - tag = str(self.build_name if self.build_name is not None else - f"{'_'.join(f'{bu[0]}-{bu[1]}' for bu in sorted_specs)}__{config.get('velocity:system')}-{config.get('velocity:distro')}") + tag = str( + self.build_name + if self.build_name is not None + else "{}__{}-{}".format( + "_".join(f"{bu.name}-{bu.version}" for bu in reversed(self.build_units)), + config.get("velocity:system"), + config.get("velocity:distro"), + ) + ) name = self.backend_engine.format_image_name(Path(pwd.absolute()), tag) else: - name = self.backend_engine.format_image_name(Path.joinpath(self.build_dir, u.id), u.id) - + name = self.backend_engine.format_image_name( + Path.joinpath(self.build_dir, "{}-{}-{}".format(u.name, u.version, u.id)), + u.id + ) self._build_image(u, last, name) - if not self.dry_run and not self.leave_tags and last is not None: - run(self.backend_engine.clean_up_old_image_cmd(last)) + if not self.dry_run and self.remove_tags and last is not None: + run(self.backend_engine.clean_up_old_image_tag(last)) last = name # go back to the starting dir os.chdir(pwd) def _build_image(self, unit: Image, src_image: str, name: str): + """Build an individual image.""" # print start of build - p1print([ - TextBlock(f"{unit.id}", fore=Fore.RED, style=Style.BRIGHT), - TextBlock(f": BUILD "), - TextBlock(f"{unit.name}@{unit.version}", fore=Fore.MAGENTA, style=Style.BRIGHT), - TextBlock(f"{' --DRY-RUN' if self.dry_run else ''} ...") - ]) + header_print( + [ + TextBlock(unit.id, fore=Fore.RED, style=Style.BRIGHT), + TextBlock(": BUILD "), + TextBlock("{}@{}".format(unit.name, unit.version), fore=Fore.MAGENTA, style=Style.BRIGHT), + TextBlock("{} ...".format(" --DRY-RUN" if self.dry_run else "")), + ] + ) start = timer() # create build dir and go to it - build_sub_dir = Path.joinpath(self.build_dir, unit.id) + build_sub_dir = Path.joinpath(self.build_dir, "{}-{}-{}".format(unit.name, unit.version, unit.id)) build_sub_dir.mkdir(mode=0o744, exist_ok=True) os.chdir(build_sub_dir) # copy additional files - if len(unit.files) > 0 and Path.joinpath(unit.path, "files").is_dir(): - p1print([ - TextBlock(f"{unit.id}", fore=Fore.RED, style=Style.BRIGHT), - TextBlock(f": COPYING FILES ...") - ]) - + if len(unit.files) > 0: + header_print([TextBlock(unit.id, fore=Fore.RED, style=Style.BRIGHT), TextBlock(": COPYING FILES ...")]) for entry in unit.files: # print copy operation if self.verbose: - sp1print([ - TextBlock('DIR: ' if Path.joinpath(unit.path, "files", entry).is_dir() else 'FILE: ', fore=Fore.YELLOW, style=Style.BRIGHT), - TextBlock(f"{Path.joinpath(unit.path, 'files', entry).absolute()}", fore=Fore.GREEN), - TextBlock(f" -> ", fore=Fore.YELLOW, style=Style.BRIGHT), - TextBlock(f"{Path.joinpath(build_sub_dir, entry).absolute()}", fore=Fore.GREEN) - ]) + indent_print( + [ + TextBlock( + "DIR: " if Path.joinpath(unit.path, "files", entry).is_dir() else "FILE: ", + fore=Fore.YELLOW, + style=Style.BRIGHT, + ), + TextBlock(str(Path.joinpath(unit.path, "files", entry).absolute()), fore=Fore.GREEN), + TextBlock(" -> ", fore=Fore.YELLOW, style=Style.BRIGHT), + TextBlock(str(Path.joinpath(build_sub_dir, entry).absolute()), fore=Fore.GREEN), + ] + ) if Path.joinpath(unit.path, "files", entry).is_dir(): shutil.copytree(Path.joinpath(unit.path, "files", entry), Path.joinpath(build_sub_dir, entry)) else: shutil.copy(Path.joinpath(unit.path, "files", entry), Path.joinpath(build_sub_dir, entry)) # parse template and create script... - p1print([ - TextBlock(f"{unit.id}", fore=Fore.RED, style=Style.BRIGHT), - TextBlock(f": GENERATING SCRIPT ...") - ]) + header_print([TextBlock(unit.id, fore=Fore.RED, style=Style.BRIGHT), TextBlock(": GENERATING SCRIPT ...")]) if self.verbose: - sp1print([ - TextBlock('SCRIPT: ', fore=Fore.YELLOW, style=Style.BRIGHT), - TextBlock(f"{Path.joinpath(build_sub_dir, 'script')}", fore=Fore.GREEN) - ]) + indent_print( + [ + TextBlock("SCRIPT: ", fore=Fore.YELLOW, style=Style.BRIGHT), + TextBlock(str(Path.joinpath(build_sub_dir, "script")), fore=Fore.GREEN), + ] + ) # get and update script variables script_variables = unit.variables.copy() - script_variables.update({'__name__': unit.name}) - script_variables.update({'__version__': str(unit.version)}) - script_variables.update({'__timestamp__': str(datetime.datetime.now())}) - script_variables.update({'__threads__': str(int(os.cpu_count() * 0.75) if int(os.cpu_count() * 0.75) < 16 else 16)}) + script_variables.update({"__name__": unit.name}) + script_variables.update({"__version__": str(unit.version)}) + script_variables.update({"__timestamp__": str(datetime.datetime.now())}) + script_variables.update( + {"__threads__": str(int(os.cpu_count() * 0.75) if int(os.cpu_count() * 0.75) < 16 else 16)} + ) if src_image is not None: - script_variables.update({'__base__': src_image}) + script_variables.update({"__base__": src_image}) - script = self.backend_engine.generate_script( - Path.joinpath(unit.path, 'templates', f'{unit.template}.vtmp'), - script_variables - ) + script = self.backend_engine.generate_script(unit, script_variables) # write out script - with open(Path.joinpath(build_sub_dir, 'script'), 'w') as out_file: + with open(Path.joinpath(build_sub_dir, "script"), "w") as out_file: for line in script: if self.verbose: - sp1print([ - TextBlock(line, fore=Fore.BLUE, style=Style.DIM) - ]) - out_file.writelines(line + '\n') + indent_print([TextBlock(line, fore=Fore.BLUE, style=Style.DIM)]) + out_file.writelines(line + "\n") # run prolog & build build_cmd = self.backend_engine.generate_build_cmd( - str(Path.joinpath(build_sub_dir, 'script')), - name, - unit.arguments + str(Path.joinpath(build_sub_dir, "script")), name, unit.arguments ) - build_file_path = Path.joinpath(build_sub_dir, 'build') - build_contents = ['#!/usr/bin/env bash'] - + build_file_path = Path.joinpath(build_sub_dir, "build") + build_contents = ["#!/usr/bin/env bash"] if unit.prolog is not None: - p1print([ - TextBlock(f"{unit.id}", fore=Fore.RED, style=Style.BRIGHT), - TextBlock(f": RUNNING PROLOG ... && BUILDING ...") - ]) - build_contents.append(unit.prolog.strip('\n')) + header_print( + [ + TextBlock(unit.id, fore=Fore.RED, style=Style.BRIGHT), + TextBlock(": RUNNING PROLOG ... && BUILDING ..."), + ] + ) + build_contents.append(unit.prolog.strip("\n")) build_contents.append(build_cmd) else: - p1print([ - TextBlock(f"{unit.id}", fore=Fore.RED, style=Style.BRIGHT), - TextBlock(f": BUILDING ...") - ]) + header_print([TextBlock(unit.id, fore=Fore.RED, style=Style.BRIGHT), TextBlock(": BUILDING ...")]) build_contents.append(build_cmd) - with open(build_file_path, 'w') as build_file: + with open(build_file_path, "w") as build_file: for line in build_contents: - build_file.write(line + '\n') + build_file.write(line + "\n") if self.verbose: - sp1print([ - TextBlock(line, fore=Fore.YELLOW, style=Style.BRIGHT) - ]) + indent_print([TextBlock(line, fore=Fore.YELLOW, style=Style.BRIGHT)]) build_file_path.chmod(0o744) if not self.dry_run: - run(str(build_file_path.absolute()), log_file=Path.joinpath(build_sub_dir, 'log'), verbose=self.verbose) + if self.backend_engine.build_exists(name): + if self.verbose: + indent_print( + [TextBlock("Using cached image {} ...".format(name), fore=Fore.GREEN, style=Style.DIM)] + ) + else: + run(str(build_file_path.absolute()), log_file=Path.joinpath(build_sub_dir, "log"), verbose=self.verbose) end = timer() - p1print([ - TextBlock(f"{unit.id}", fore=Fore.RED, style=Style.BRIGHT), - TextBlock(f": IMAGE "), - TextBlock(f"{name}", fore=Fore.MAGENTA, style=Style.BRIGHT), - TextBlock(' ('), - TextBlock(f"{unit.name}@{unit.version}", fore=Fore.MAGENTA, style=Style.BRIGHT), - TextBlock(f") BUILT ["), - TextBlock(f"{datetime.timedelta(seconds=round(end - start))}", fore=Fore.MAGENTA, style=Style.BRIGHT), - TextBlock(']') - ]) + header_print( + [ + TextBlock(unit.id, fore=Fore.RED, style=Style.BRIGHT), + TextBlock(": IMAGE "), + TextBlock(name, fore=Fore.MAGENTA, style=Style.BRIGHT), + TextBlock(" ("), + TextBlock("{}@{}".format(unit.name, unit.version), fore=Fore.MAGENTA, style=Style.BRIGHT), + TextBlock(") BUILT ["), + TextBlock(str(datetime.timedelta(seconds=round(end - start))), fore=Fore.MAGENTA, style=Style.BRIGHT), + TextBlock("]"), + ] + ) print() # new line diff --git a/src/velocity/_config.py b/src/velocity/_config.py index 09887cf..8ced3c8 100644 --- a/src/velocity/_config.py +++ b/src/velocity/_config.py @@ -1,10 +1,11 @@ """Config organizer. Provides default config object for velocity.""" from loguru import logger -from ._exceptions import InvalidConfigIdentifier from platform import processor as arch from pathlib import Path from os import getlogin as get_username +from importlib.metadata import version +from ._exceptions import InvalidConfigIdentifier class Config: @@ -75,8 +76,9 @@ def __str__(self) -> str: "verbose": False, "debug": "WARNING", "config_dir": Path.home().joinpath(".velocity", "config"), - "image_dir": Path.home().joinpath(".velocity", "images"), + "image_path": Path.home().joinpath(".velocity", "images"), "build_dir": Path("/tmp").joinpath(get_username(), "velocity"), + "version": version("velocity") }, ) config = _config From c363371b2a612823189360eb8149a646450fa24f Mon Sep 17 00:00:00 2001 From: Asa Rentschler Date: Mon, 9 Sep 2024 14:33:45 -0400 Subject: [PATCH 04/10] Cleaned up graph a little --- src/velocity/_graph.py | 436 ++++++++++++++++++++--------------------- tests/test__graph.py | 3 +- 2 files changed, 216 insertions(+), 223 deletions(-) diff --git a/src/velocity/_graph.py b/src/velocity/_graph.py index 1111c17..bc513dd 100644 --- a/src/velocity/_graph.py +++ b/src/velocity/_graph.py @@ -1,29 +1,22 @@ """Graph library and tools for dependency graph""" + from loguru import logger from re import split as re_split, fullmatch as re_fullmatch, Match as ReMatch -from ._exceptions import ( - InvalidImageVersionError, - InvalidDependencySpecification, - EdgeViolatesDAG, - NoAvailableBuild -) from pathlib import Path from hashlib import sha256 from typing_extensions import Self +from yaml import safe_load as yaml_safe_load +from copy import deepcopy +from enum import Enum from networkx import ( DiGraph as nx_DiGraph, is_directed_acyclic_graph as nx_is_directed_acyclic_graph, find_cycle as nx_find_cycle, neighbors as nx_neighbors, - has_path as nx_has_path + has_path as nx_has_path, ) -from yaml import safe_load as yaml_safe_load from ._config import config -from copy import deepcopy -from enum import Enum - -VERSION_REGEX = \ - r"^(?P[0-9]+)(?:\.(?P[0-9]+)(:?\.(?P[0-9]+))?)?(?:-(?P[a-zA-Z0-9]+))?$" +from ._exceptions import InvalidImageVersionError, CannotFindDependency, EdgeViolatesDAG, NoAvailableBuild def get_permutations(idx: int, sets: list[list]): @@ -48,15 +41,19 @@ def get_permutations(idx: int, sets: list[list]): class Version: - """Version class for images.""" + """Version class.""" + def __init__(self, version_specifier: str) -> None: self.vs = version_specifier try: - version_dict: dict = re_fullmatch(VERSION_REGEX, version_specifier).groupdict() - self.major: int | None = int(version_dict['major']) if version_dict['major'] is not None else None - self.minor: int | None = int(version_dict['minor']) if version_dict['minor'] is not None else None - self.patch: int | None = int(version_dict['patch']) if version_dict['patch'] is not None else None - self.suffix: str | None = str(version_dict['suffix']) if version_dict['suffix'] is not None else None + version_dict: dict = re_fullmatch( + r"^(?P[0-9]+)(?:\.(?P[0-9]+)(:?\.(?P[0-9]+))?)?(?:-(?P[a-zA-Z0-9]+))?$", + version_specifier, + ).groupdict() + self.major: int | None = int(version_dict["major"]) if version_dict["major"] is not None else None + self.minor: int | None = int(version_dict["minor"]) if version_dict["minor"] is not None else None + self.patch: int | None = int(version_dict["patch"]) if version_dict["patch"] is not None else None + self.suffix: str | None = str(version_dict["suffix"]) if version_dict["suffix"] is not None else None logger.trace("Version: {}".format(self.__str__())) except AttributeError: self.major: int | None = None @@ -74,7 +71,7 @@ def vcs(self) -> str: self.major if self.major is not None else "#", self.minor if self.minor is not None else "#", self.patch if self.patch is not None else "#", - self.suffix if self.suffix is not None else "~" + self.suffix if self.suffix is not None else "~", ) def preferred(self, other) -> bool: @@ -105,52 +102,55 @@ def _cut_vcses_to_size(cls, one: Self, two: Self) -> tuple[str, str]: def __eq__(self, other) -> bool: if not isinstance(other, Version): - raise TypeError(f"'>' not supported between instances of " - f"'{type(self).__name__}' and '{type(other).__name__}'") + raise TypeError( + f"'>' not supported between instances of " f"'{type(self).__name__}' and '{type(other).__name__}'" + ) s_vcs, o_vcs = self._cut_vcses_to_size(self, other) return s_vcs == o_vcs def __ne__(self, other) -> bool: if not isinstance(other, Version): - raise TypeError(f"'>' not supported between instances of " - f"'{type(self).__name__}' and '{type(other).__name__}'") + raise TypeError( + f"'>' not supported between instances of " f"'{type(self).__name__}' and '{type(other).__name__}'" + ) s_vcs, o_vcs = self._cut_vcses_to_size(self, other) return s_vcs != o_vcs def __gt__(self, other) -> bool: if not isinstance(other, Version): - raise TypeError(f"'>' not supported between instances of " - f"'{type(self).__name__}' and '{type(other).__name__}'") + raise TypeError( + f"'>' not supported between instances of " f"'{type(self).__name__}' and '{type(other).__name__}'" + ) return self.vcs > other.vcs and not self.__eq__(other) def __ge__(self, other) -> bool: if not isinstance(other, Version): - raise TypeError(f"'>' not supported between instances of " - f"'{type(self).__name__}' and '{type(other).__name__}'") + raise TypeError( + f"'>' not supported between instances of " f"'{type(self).__name__}' and '{type(other).__name__}'" + ) return self.vcs > other.vcs or self.__eq__(other) def __lt__(self, other) -> bool: if not isinstance(other, Version): - raise TypeError(f"'>' not supported between instances of " - f"'{type(self).__name__}' and '{type(other).__name__}'") + raise TypeError( + f"'>' not supported between instances of " f"'{type(self).__name__}' and '{type(other).__name__}'" + ) return self.vcs < other.vcs and not self.__eq__(other) def __le__(self, other) -> bool: if not isinstance(other, Version): - raise TypeError(f"'>' not supported between instances of " - f"'{type(self).__name__}' and '{type(other).__name__}'") + raise TypeError( + f"'>' not supported between instances of " f"'{type(self).__name__}' and '{type(other).__name__}'" + ) return self.vcs < other.vcs or self.__eq__(other) def __str__(self) -> str: return self.vs -class Spec: - def __init__(self): - pass - - class Image: + """Velocity container image.""" + def __init__(self, name: str, version: str, system: str, backend: str, distro: str, path: str) -> None: # foundational self.name: str = name @@ -158,22 +158,24 @@ def __init__(self, name: str, version: str, system: str, backend: str, distro: s self.system: str = system self.backend: str = backend self.distro: str = distro - self.dependencies: list[str] = list() + self.dependencies: set[str] = set() # additional self.variables: dict[str, str] = dict() - self.arguments: list[str] = list() + self.arguments: set[str] = set() self.template: str = "default" - self.files: list[str] = list() + self.files: set[str] = set() self.prolog: str | None = None + self.underlay: int | None = None # sum of the ids this image will be built on # metadata self.path: Path = Path(path) def satisfies(self, spec: str) -> bool: """Test if this node satisfies the given spec.""" - name_version_regex: str = \ + name_version_regex: str = ( r"^(?P{})(?:(?:@(?P[\d\.]+)(?!@))?(?:@?(?P:)(?P[\d\.]+)?)?)?$".format(self.name) + ) system_regex: str = r"^system=(?P[a-zA-Z0-9]+)$" backend_regex: str = r"^backend=(?P[a-zA-Z0-9]+)$" distro_regex: str = r"^distro=(?P[a-zA-Z0-9]+)$" @@ -236,7 +238,7 @@ def satisfies(self, spec: str) -> bool: if res.group("name") == dep: matched = True if matched: - continue # match is found so continue + continue # match is found so continue # if we get here this part has not been handled return False @@ -245,26 +247,24 @@ def satisfies(self, spec: str) -> bool: return True def apply_constraint(self, conditional: str, _type: str, spec: str) -> bool: + """Evaluate and apply constraints. Return True if a constraint changes the dependencies.""" if self.satisfies(conditional): if _type == "dependency": - self.dependencies.append(spec) + if spec not in self.dependencies: + self.dependencies.add(spec) + return True elif _type == "variable": parts = spec.split("=") self.variables[parts[0]] = parts[1] elif _type == "argument": - self.arguments.append(spec) + self.arguments.add(spec) elif _type == "template": self.template = spec elif self.files == "file": - self.files.append(spec) + self.files.add(spec) elif _type == "prolog": self.prolog = spec - else: - # not a valid type - return False - else: - # spec not added - return False + return False @property def hash(self) -> str: @@ -286,12 +286,14 @@ def hash(self) -> str: hash_list.append(None) hash_list.append(",".join(str(x) for x in self.files)) hash_list.append(self.prolog) + hash_list.append(self.underlay) hash_str: str = "|".join(str(x) for x in hash_list) return sha256(hash_str.encode()).hexdigest() @property def id(self) -> str: + """Short hash.""" return self.hash[:7] def __hash__(self) -> int: @@ -315,45 +317,42 @@ def __str__(self) -> str: self.system, self.backend, self.distro, - "".join(" ^{}".format(x) for x in self.dependencies) if len(self.dependencies) > 0 else "" + "".join(" ^{}".format(x) for x in self.dependencies) if len(self.dependencies) > 0 else "", ) class DepOp(Enum): - """ - Specify dependency options - """ - EQ = '=' - GE = '>=' - LE = '<=' + """Dependency options.""" + + EQ = "=" + GE = ">=" + LE = "<=" UN = None class Target: - """ - Build specification targets - """ + """Build targets.""" def __init__(self, node: Image, op: DepOp): self.node = node self.op = op def __str__(self): - return f'Target: {self.op} -> {str(self.node)}' + return "Target: {} -> {}".format(self.op, self.node) class ImageGraph(nx_DiGraph): + """Image dependency graph.""" + def __init__(self, **kwargs) -> None: super().__init__(**kwargs) - def add_edge(self, u_of_edge: Image, v_of_edge: Image, **attr) -> None: - # check that edge endpoint are in graph + def add_edge(self, u_of_edge: Image, v_of_edge: Image, **kwargs) -> None: + # check that edge endpoints are in graph if self.has_node(u_of_edge) and self.has_node(v_of_edge): - super().add_edge(u_of_edge, v_of_edge, **attr) + super().add_edge(u_of_edge, v_of_edge, **kwargs) else: - raise InvalidDependencySpecification(f'{v_of_edge.name}@{v_of_edge.version}', - u_of_edge.name, u_of_edge.version, - Path.joinpath(u_of_edge.path, 'specs.yaml')) + raise CannotFindDependency("Cannot find dependency {} for {}".format(v_of_edge, u_of_edge)) # check that graph is still a DAG if not nx_is_directed_acyclic_graph(self): @@ -361,6 +360,7 @@ def add_edge(self, u_of_edge: Image, v_of_edge: Image, **attr) -> None: raise EdgeViolatesDAG(u_of_edge, v_of_edge, cycle) def get_similar_nodes(self, node: Image) -> set: + """Get all nodes with the same name.""" similar = set() for n in self.nodes: if n.satisfies(node.name): @@ -368,31 +368,27 @@ def get_similar_nodes(self, node: Image) -> set: return similar def get_dependencies(self, node: Image) -> set[Image]: + """Get all dependencies for an image.""" # had to add this wrapper because nx.neighbors was dropping the attributes of some nodes (cuda, python) deps = set() for node in nx_neighbors(self, node): for n in self.nodes: if n == node: deps.add(n) - # will contact the maintainers of networkx at some point - + # TODO will contact the maintainers of networkx at some point return deps def is_above(self, u_node: Image, v_node: Image) -> bool: - """ - Test if one node is above another in the dependency tree - """ + """Test if one node is above another in the dependency tree.""" return nx_has_path(self, u_node, v_node) def _is_valid_build_tuple(self, bt: tuple[Image]) -> bool: - """ - Verify that all the dependencies of a build tuple can be met. - """ + """Verify that all the dependencies of a build tuple can be met.""" valid = True # check for similar images for i0 in range(len(bt)): - for i2 in bt[i0 + 1:]: + for i2 in bt[i0 + 1 :]: if bt[i0].satisfies(i2.name): valid = False @@ -424,7 +420,7 @@ def _is_valid_build_tuple(self, bt: tuple[Image]) -> bool: return valid def create_build_recipe(self, targets: list[Target]) -> tuple: - + """Create a build recipe.""" # check if all the targets exist for node in targets: if len(self.get_similar_nodes(node.node)) < 1: @@ -484,7 +480,6 @@ def create_build_recipe(self, targets: list[Target]) -> tuple: # return valid build tuple for p in permutations: - # clean up permutation clean_p = set() for n in p: @@ -522,6 +517,8 @@ def create_build_recipe(self, targets: list[Target]) -> tuple: class ImageRepo: + """Image repository.""" + def __init__(self) -> None: # constraint(image, condition, type, spec, scope) self.constraints: list[tuple[str, str, str, str, str]] = list() @@ -533,7 +530,7 @@ def import_from_dir(self, path: str) -> None: if not p.is_dir(): raise NotADirectoryError(f"Image path {path} is not a directory!") - for name in [x for x in p.iterdir() if x.is_dir() and x.name[0] != '.']: + for name in [x for x in p.iterdir() if x.is_dir() and x.name[0] != "."]: with open(name.joinpath("specs.yaml"), "r") as fi: specs = yaml_safe_load(fi) # add versions @@ -544,7 +541,7 @@ def import_from_dir(self, path: str) -> None: config.get("velocity:system"), config.get("velocity:backend"), config.get("velocity:distro"), - str(name) + str(name), ) if "when" in version: if image.satisfies(version["when"]): @@ -556,189 +553,179 @@ def import_from_dir(self, path: str) -> None: # dependencies if "dependencies" in specs: for dependency in specs["dependencies"]: - self.constraints.append(( - name.name, - dependency["when"] if "when" in dependency else "", - "dependency", - dependency["spec"], - dependency["scope"] if "scope" in dependency else "image" - )) + self.constraints.append( + ( + name.name, + dependency["when"] if "when" in dependency else "", + "dependency", + dependency["spec"], + dependency["scope"] if "scope" in dependency else "image", + ) + ) # templates if "templates" in specs: for template in specs["templates"]: - self.constraints.append(( - name.name, - template["when"] if "when" in template else "", - "template", - template["path"], - template["scope"] if "scope" in template else "image" - )) + self.constraints.append( + ( + name.name, + template["when"] if "when" in template else "", + "template", + template["path"], + template["scope"] if "scope" in template else "image", + ) + ) # arguments if "arguments" in specs: for argument in specs["arguments"]: - self.constraints.append(( - name.name, - argument["when"] if "when" in argument else "", - "argument", - argument["value"], - argument["scope"] if "scope" in argument else "image" - )) + self.constraints.append( + ( + name.name, + argument["when"] if "when" in argument else "", + "argument", + argument["value"], + argument["scope"] if "scope" in argument else "image", + ) + ) # variables if "variables" in specs: for variable in specs["variables"]: - self.constraints.append(( - name.name, - variable["when"] if "when" in variable else "", - "variable", - "{}={}".format(variable["name"], variable["value"]), - variable["scope"] if "scope" in variable else "image" - )) + self.constraints.append( + ( + name.name, + variable["when"] if "when" in variable else "", + "variable", + "{}={}".format(variable["name"], variable["value"]), + variable["scope"] if "scope" in variable else "image", + ) + ) # files if "files" in specs: for file in specs["files"]: - self.constraints.append(( - name.name, - file["when"] if "when" in file else "", - "file", - file["name"], - file["scope"] if "scope" in file else "image" - )) + self.constraints.append( + ( + name.name, + file["when"] if "when" in file else "", + "file", + file["name"], + file["scope"] if "scope" in file else "image", + ) + ) # prologs if "prologs" in specs: for prolog in specs["prologs"]: - self.constraints.append(( - name.name, - prolog["when"] if "when" in prolog else "", - "prolog", - prolog["script"], - prolog["scope"] if "scope" in prolog else "image" - )) + self.constraints.append( + ( + name.name, + prolog["when"] if "when" in prolog else "", + "prolog", + prolog["script"], + prolog["scope"] if "scope" in prolog else "image", + ) + ) def create_build_recipe(self, targets: list[str]) -> tuple: - images = deepcopy(self.images) + """Create an ordered build recipe of images.""" + images: set[Image] = deepcopy(self.images) - build_targets = list() - targs = list() - for t in targets: + build_targets: list[Target] = list() + targs: list[Image] = list() + for target in targets: res = re_fullmatch( r"^(?P[a-zA-Z0-9-]+)(?:(?:@(?P[\d\.]+)(?!@))?(?:@?(?P:)(?P[\d\.]+)?)?)?$", - t + target, ) if res is not None: gd: dict = res.groupdict() - if gd["left"] is not None and gd["right"] is None: # n@v: or n@v + # n@v: or n@v + if gd["left"] is not None and gd["right"] is None: + t = Image( + gd["name"], + gd["left"], + config.get("velocity:system"), + config.get("velocity:backend"), + config.get("velocity:distro"), + "", + ) + targs.append(t) if gd["colen"] is not None: - t = Image( - gd["name"], - gd["left"], - config.get("velocity:system"), - config.get("velocity:backend"), - config.get("velocity:distro"), - "" - ) - build_targets.append(Target( - t, - DepOp.GE - )) - targs.append(t) + build_targets.append(Target(t, DepOp.GE)) else: - t = Image( - gd["name"], - gd["left"], - config.get("velocity:system"), - config.get("velocity:backend"), - config.get("velocity:distro"), - "" - ) - build_targets.append(Target( - t, - DepOp.EQ - )) - targs.append(t) - elif gd["left"] is None and gd["right"] is not None: # n@:v + build_targets.append(Target(t, DepOp.EQ)) + # n@:v + elif gd["left"] is None and gd["right"] is not None: if gd["colen"] is not None: t = Image( - gd["name"], - gd["right"], - config.get("velocity:system"), - config.get("velocity:backend"), - config.get("velocity:distro"), - "" - ) - build_targets.append(Target( - t, - DepOp.LE - )) + gd["name"], + gd["right"], + config.get("velocity:system"), + config.get("velocity:backend"), + config.get("velocity:distro"), + "", + ) + build_targets.append(Target(t, DepOp.LE)) targs.append(t) else: - raise InvalidImageVersionError() - elif gd["left"] is None and gd["right"] is None: # n + raise InvalidImageVersionError("Invalid version '{}'.".format(target)) + # n + elif gd["left"] is None and gd["right"] is None: if gd["colen"] is not None: - raise InvalidImageVersionError() + raise InvalidImageVersionError("Invalid version '{}'.".format(target)) else: t = Image( - gd["name"], - "", - config.get("velocity:system"), - config.get("velocity:backend"), - config.get("velocity:distro"), - "" - ) - build_targets.append(Target( - t, - DepOp.EQ - )) - targs.append(t) - else: # n@v:v - t = Image( gd["name"], - gd["left"], + "", config.get("velocity:system"), config.get("velocity:backend"), config.get("velocity:distro"), - "" + "", ) - build_targets.append(Target( - t, - DepOp.GE - )) + build_targets.append(Target(t, DepOp.UN)) + targs.append(t) + # n@v:v + else: + t = Image( + gd["name"], + gd["left"], + config.get("velocity:system"), + config.get("velocity:backend"), + config.get("velocity:distro"), + "", + ) + build_targets.append(Target(t, DepOp.GE)) targs.append(t) t = Image( - gd["name"], - gd["right"], - config.get("velocity:system"), - config.get("velocity:backend"), - config.get("velocity:distro"), - "" - ) - build_targets.append(Target( - t, - DepOp.LE - )) + gd["name"], + gd["right"], + config.get("velocity:system"), + config.get("velocity:backend"), + config.get("velocity:distro"), + "", + ) + build_targets.append(Target(t, DepOp.LE)) targs.append(t) else: raise NoAvailableBuild("No available build!") # apply constraints - for constraint in self.constraints: - if constraint[4] == "build": - for targ in targs: - if targ.satisfies(constraint[1]): - for image in images: - image.apply_constraint( - constraint[0], - constraint[2], - constraint[3] - ) - - else: # default is image - for image in images: - image.apply_constraint( - "{} {}".format(constraint[0], constraint[1]), - constraint[2], - constraint[3] - ) - + images_changed: bool = True + while images_changed: + images_changed = False + for constraint in self.constraints: + if constraint[4] == "build": + for targ in targs: + if targ.satisfies(constraint[1]): + for image in images: + if image.apply_constraint(constraint[0], constraint[2], constraint[3]): + images_changed = True + # default is constraint[4] == "image" + else: + for image in images: + if image.apply_constraint( + "{} {}".format(constraint[0], constraint[1]), constraint[2], constraint[3] + ): + images_changed = True + + # create graph ig = ImageGraph() for image in images: ig.add_node(image) @@ -748,5 +735,12 @@ def create_build_recipe(self, targets: list[str]) -> tuple: if di.satisfies(dep): ig.add_edge(image, di) - return ig.create_build_recipe(build_targets) + bt: tuple[Image] = ig.create_build_recipe(build_targets) + + # update images so that their hash includes the layers below them + cumulative_deps: int = 0 + for b in bt: + b.underlay = cumulative_deps + cumulative_deps = cumulative_deps + int(b.id, 16) + return bt diff --git a/tests/test__graph.py b/tests/test__graph.py index e94a65f..d188d5b 100644 --- a/tests/test__graph.py +++ b/tests/test__graph.py @@ -1,7 +1,6 @@ from unittest import TestCase from re import compile from src.velocity._graph import ( - VERSION_REGEX, Version, Image ) @@ -9,7 +8,7 @@ class Test(TestCase): def test_version_regex(self): - p = compile(VERSION_REGEX) + p = compile(r"^(?P[0-9]+)(?:\.(?P[0-9]+)(:?\.(?P[0-9]+))?)?(?:-(?P[a-zA-Z0-9]+))?$") # test 2.3.5-s90er m = p.search("2.3.5-s90er") From 6acb94d837c49ce3cd49a4e1bd6e3497f28c3b94 Mon Sep 17 00:00:00 2001 From: Asa Rentschler Date: Fri, 13 Sep 2024 14:35:08 -0400 Subject: [PATCH 05/10] Major bug fixes and improvments. Getting close! --- src/velocity/__init__.py | 6 +- src/velocity/__main__.py | 441 ++++++++++++++---------------------- src/velocity/_backends.py | 35 ++- src/velocity/_build.py | 51 +++-- src/velocity/_config.py | 82 +++++-- src/velocity/_exceptions.py | 59 +++-- src/velocity/_graph.py | 227 +++++++++++++------ 7 files changed, 477 insertions(+), 424 deletions(-) diff --git a/src/velocity/__init__.py b/src/velocity/__init__.py index 990b15e..28386da 100644 --- a/src/velocity/__init__.py +++ b/src/velocity/__init__.py @@ -1,5 +1,5 @@ +from loguru import logger; logger.disable("velocity") # disable logging at the module level from ._config import config -__all__ = [ - config -] + +__all__ = [config] diff --git a/src/velocity/__main__.py b/src/velocity/__main__.py index 7fbdd35..0c82b96 100644 --- a/src/velocity/__main__.py +++ b/src/velocity/__main__.py @@ -1,285 +1,180 @@ +"""Run velocity as a script.""" + import argparse -import re -import os -import editor -from pathlib import Path +import sys +from docutils.parsers.rst.directives.images import Image +from loguru import logger +from importlib.metadata import version from colorama import Fore, Style -from ._graph import ImageRepo, Target, DepOp -from ._build import Builder -from ._print import p1print, sp1print, TextBlock, wprint, print_text_blocks +from ._graph import ImageRepo +from ._build import ImageBuilder +from ._print import header_print, indent_print, TextBlock, bare_print from ._config import config - ############################################################ -# Main +# Parse Args ############################################################ -if __name__ == '__main__': - - ############################################################ - # Load Config - ############################################################ - if os.getenv('VELOCITY_SYSTEM') is not None: - config.set("velocity:system", os.getenv('VELOCITY_SYSTEM')) - if os.getenv('VELOCITY_BACKEND') is not None: - config.set("velocity:backend", os.getenv('VELOCITY_BACKEND')) - if os.getenv('VELOCITY_DISTRO') is not None: - config.set("velocity:distro", os.getenv('VELOCITY_DISTRO')) - if os.getenv('VELOCITY_BUILD_DIR') is not None: - config.set("velocity:build_dir", os.getenv('VELOCITY_BUILD_DIR')) - - ############################################################ - # Parse Args - ############################################################ - parser = argparse.ArgumentParser( - prog='velocity', - description='Build tool for OLCF containers', - epilog='See (https://gitlab.ccs.ornl.gov/saue-software/velocity)') - parser.add_argument('-v', '--version', action='version', - version=f"%(prog)s {config.get('velocity:version')}", help="program version") - parser.add_argument('-b', '--backend', action='store') - parser.add_argument('-s', '--system', action='store') - parser.add_argument('-d', '--distro', action='store') - - # create sub_parsers - sub_parsers = parser.add_subparsers(dest='subcommand') - - # create build_parser - build_parser = sub_parsers.add_parser('build', help="build specified container image") - build_parser.add_argument('-d', '--dry-run', action='store_true', help="dry run build system") - build_parser.add_argument('targets', type=str, nargs='+', help='build targets') - build_parser.add_argument('-n', '--name', action='store', help='name of complete image') - build_parser.add_argument('-l', '--leave-tags', action='store_true', - help="do not clean up intermediate build tags (only applies to podman)") - build_parser.add_argument('-v', '--verbose', action='store_true', help="print helpful debug/runtime information") - - # create avail_parser - avail_parser = sub_parsers.add_parser('avail', help="lookup available images") - - # create spec_parser - spec_parser = sub_parsers.add_parser('spec', help="lookup image dependencies") - spec_parser.add_argument('targets', type=str, nargs='+', help='spec targets') - - # create edit_parser - edit_parser = sub_parsers.add_parser('edit', help="edit image files") - edit_parser.add_argument('target', help='image to edit') - edit_parser.add_argument('-s', '--specification', action='store_true', help="edit the specifications file") - - # create create_parser - edit_parser = sub_parsers.add_parser('create', help="create a new image with default template") - edit_parser.add_argument('name', help='name of image to create') - edit_parser.add_argument('version', help='version of image to create') - - # parse args - args = parser.parse_args() +parser = argparse.ArgumentParser( + prog="velocity", + description="build tool for OLCF containers", + epilog="See https://github.com/olcf/velocity", +) +parser.add_argument( + "-v", "--version", action="version", version=f"%(prog)s {version('velocity')}", help="program version" +) +parser.add_argument( + "-D", + "--debug", + choices=["TRACE", "DEBUG", "INFO", "SUCCESS", "WARNING", "ERROR", "CRITICAL"], + help="set debug output level", +) +parser.add_argument("-b", "--backend") +parser.add_argument("-s", "--system") +parser.add_argument("-d", "--distro") + +# create sub_parsers +sub_parsers = parser.add_subparsers(dest="subcommand") + +# create build_parser +build_parser = sub_parsers.add_parser("build", help="build specified container image") +build_parser.add_argument("-d", "--dry-run", action="store_true", help="dry run build system") +build_parser.add_argument("targets", type=str, nargs="+", help="build targets") +build_parser.add_argument("-n", "--name", action="store", help="name of complete image") +build_parser.add_argument( + "-l", "--leave-tags", action="store_true", help="do not clean up intermediate build tags (only applies to podman)" +) +build_parser.add_argument("-v", "--verbose", action="store_true", help="print helpful debug/runtime information") + +# create avail_parser +avail_parser = sub_parsers.add_parser("avail", help="lookup available images") + +# create spec_parser +spec_parser = sub_parsers.add_parser("spec", help="lookup image dependencies") +spec_parser.add_argument("targets", type=str, nargs="+", help="spec targets") + +# parse args +args = parser.parse_args() - ############################################################ - # apply user run time arguments over settings - ############################################################ - if args.system is not None: - config.set("velocity:system", args.system) - if args.backend is not None: - config.set("velocity:backend", args.backend) - if args.distro is not None: - config.set("velocity:distro", args.distro) +############################################################ +# apply user run time arguments over settings +############################################################ +if args.debug is not None: + config.set("velocity:debug", args.debug) +if args.system is not None: + config.set("velocity:system", args.system) +if args.backend is not None: + config.set("velocity:backend", args.backend) +if args.distro is not None: + config.set("velocity:distro", args.distro) + +# setup logging and log startup +logger.enable("velocity") +logger.configure(handlers=[{"sink": sys.stdout, "level": config.get("velocity:debug")}]) +logger.debug( + "Starting velocity {},{},{}.".format( + config.get("velocity:system"), config.get("velocity:backend"), config.get("velocity:distro") + ) +) +logger.trace(config.get("")) - ############################################################ - # Load images - ############################################################ - imageRepo = ImageRepo() - imageRepo.import_from_dir("/home/xjv/PycharmProjects/velocity-images") +############################################################ +# Load images +############################################################ +imageRepo = ImageRepo() +for p in config.get("velocity:image_path").split(":"): + imageRepo.import_from_dir(p) - # print backend, system, distro - """ - p1print([ - TextBlock(f"System: "), - TextBlock(f"{SETTINGS['VELOCITY_SYSTEM']}", fore=Fore.MAGENTA, style=Style.BRIGHT) - ]) - p1print([ - TextBlock(f"Backend: "), - TextBlock(f"{SETTINGS['VELOCITY_BACKEND']}", fore=Fore.MAGENTA, style=Style.BRIGHT) - ]) - p1print([ - TextBlock(f"Distro: "), - TextBlock(f"{SETTINGS['VELOCITY_DISTRO']}", fore=Fore.MAGENTA, style=Style.BRIGHT) - ]) +############################################################ +# Handle User Commands +############################################################ +if args.subcommand == "build": + # get recipe + recipe = imageRepo.create_build_recipe(args.targets)[0] + + # print build specs + header_print([TextBlock("Build Order:")]) + for r in recipe: + indent_print([TextBlock(f"{r.name}@{r.version}", fore=Fore.MAGENTA, style=Style.BRIGHT)]) + print() # newline + + # prep builder + builder = ImageBuilder( + recipe, build_name=args.name, dry_run=args.dry_run, remove_tags=not args.leave_tags, verbose=args.verbose + ) + + # build + builder.build() + +elif args.subcommand == "avail": + # group and order + grouped = dict() + for node in imageRepo.images: + if node.name not in grouped: + grouped[node.name] = list() + grouped[node.name].append(node) + ordered = list(grouped.keys()) + ordered.sort() + + # print + for group in ordered: + header_print([TextBlock(group, fore=Fore.RED, style=Style.BRIGHT)]) + deps = grouped[group] + deps.sort() + for t in deps: + indent_print([TextBlock(t.version, fore=Fore.YELLOW, style=Style.BRIGHT)]) print() # add newline - """ - - ############################################################ - # Handle User Commands - ############################################################ - if args.subcommand == 'build': - # parse targets - - # get recipe - recipe = imageRepo.create_build_recipe(args.targets) - - # print build specs - p1print([ - TextBlock('Build Order:') - ]) - for r in recipe: - sp1print([ - TextBlock(f"{r.name}@{r.version}", fore=Fore.MAGENTA, style=Style.BRIGHT) - ]) - print() # add newline - - # prep builder - builder = Builder( - recipe, - build_name=args.name, - dry_run=args.dry_run, - leave_tags=args.leave_tags, - verbose=args.verbose - ) - # build - builder.build() - - elif args.subcommand == 'avail': - - # group and order - grouped = dict() - for node in imageRepo.images: - if node.name not in grouped: - grouped[node.name] = list() - grouped[node.name].append(node) - ordered = list(grouped.keys()) - ordered.sort() - - # print - for group in ordered: - p1print([ - TextBlock(group, fore=Fore.RED, style=Style.BRIGHT) - ]) - deps = grouped[group] - deps.sort() - for t in deps: - sp1print([ - TextBlock(t.version, fore=Fore.YELLOW, style=Style.BRIGHT) - ]) - print() # add newline - - elif args.subcommand == 'spec': - # parse targets - targets = [] - for target in args.targets: - result = re.search(r'^(.*)@(.*)([%^_=])(.*)$', target) - if result is not None: - if result[3] == '=': - targets.append(Target(Node(result[1], result[4]), DepOp.EQ)) - elif result[3] == '^': - targets.append(Target(Node(result[1], result[4]), DepOp.GE)) - elif result[3] == '_': - targets.append(Target(Node(result[1], result[4]), DepOp.LE)) - elif result[3] == '%': - targets.append(Target(Node(result[1], result[2]), DepOp.GE)) - targets.append(Target(Node(result[1], result[4]), DepOp.LE)) - else: - targets.append(Target(Node(target, ''), DepOp.UN)) - - # get recipe - recipe = imageGraph.create_build_recipe(targets) - - # flatten dependency tree - flat_dep_tree = dict() - for r in recipe: - flat_dep_tree[r.name] = set() - deps = set(imageGraph.get_dependencies(r)) - for o in deps.intersection(set(recipe)): - flat_dep_tree[r.name].add(o.name) - # get top level entries - top_level_entries = set() - deps = set() - for r in recipe: - deps.update(imageGraph.get_dependencies(r)) - for r in recipe: - if r not in deps: - top_level_entries.add(r) - - - def spec_print(seed: str, indent: int, fdt: dict, rs: tuple[Node]): - """ - Recursive function to print dep tree - """ - spec = None - for _ in rs: - if _.similar(Node(seed, '')): - spec = f"{_.name}@={_.tag}" - print(' ' + ' ' * indent, end='') - if indent == 0: - print_text_blocks([ - TextBlock('> ', fore=Fore.RED, style=Style.BRIGHT), - TextBlock(spec, fore=Fore.MAGENTA, style=Style.BRIGHT) - ]) - else: - print_text_blocks([ - TextBlock('^', fore=Fore.GREEN, style=Style.BRIGHT), - TextBlock(spec, fore=Fore.MAGENTA, style=Style.BRIGHT) - ]) - if len(fdt[seed]) > 0: - for un in fdt[seed]: - spec_print(un, (indent + 1), fdt, rs) - - - # print specs - for tl in top_level_entries: - spec_print(tl.name, 0, flat_dep_tree, recipe) - print() # add newline - - elif args.subcommand == 'edit': - - name = None - tag = None - result = re.search(r'^(.*)@=(.*)$', args.target) - if result: - name = Path(SETTINGS['VELOCITY_IMAGE_DIR']).joinpath(result[1]) - tag = Path.joinpath(name, result[2]) - else: - name = Path(SETTINGS['VELOCITY_IMAGE_DIR']).joinpath(args.target) - - if name.is_dir(): - if tag is None: - tags = list(name.iterdir()) - tags.sort() - tag = Path.joinpath(name, tags[-1]) - - if tag.is_dir(): - if args.specification: - file = Path.joinpath(tag, 'specifications.yaml') - else: - file = Path.joinpath(tag, 'templates', f"{SETTINGS['VELOCITY_DISTRO']}.vtmp") - - if file.is_file(): - editor.edit(file) - else: - raise FileNotFoundError(file) - else: - raise NotADirectoryError(tag) +elif args.subcommand == "spec": + # get recipe + recipe, graph = imageRepo.create_build_recipe(args.targets) + + # flatten dependency tree + flat_dep_tree = dict() + for r in recipe: + flat_dep_tree[r.name] = set() + deps = set(graph.get_dependencies(r)) + for o in deps.intersection(set(recipe)): + flat_dep_tree[r.name].add(o.name) + # get top level entries + top_level_entries = set() + deps = set() + for r in recipe: + deps.update(graph.get_dependencies(r)) + for r in recipe: + if r not in deps: + top_level_entries.add(r) + + def spec_print(seed: str, indent: int, fdt: dict, rs: tuple[Image]): + """ + Recursive function to print dep tree + """ + spec = "" + for _ in rs: + if _.satisfies(seed): + spec = "{}@{}".format(_.name, _.version) + print(" " + " " * indent, end="") + if indent == 0: + bare_print( + [ + TextBlock("> ", fore=Fore.RED, style=Style.BRIGHT), + TextBlock(spec, fore=Fore.MAGENTA, style=Style.BRIGHT), + ] + ) else: - raise NotADirectoryError(name) - - elif args.subcommand == 'create': - image_dir = Path(SETTINGS['VELOCITY_IMAGE_DIR']).joinpath(args.name, args.version) - if image_dir.is_dir(): - raise FileExistsError(f"'{image_dir}' already exist!") - os.mkdir(image_dir) - os.mkdir(image_dir.joinpath('templates')) - with open(image_dir.joinpath('specifications.yaml'), 'w') as spec_file: - spec_file.write('---\n') - spec_file.write('\n') - spec_file.write('build_specifications:\n') - spec_file.write(f" {SETTINGS['VELOCITY_SYSTEM']}:\n") - spec_file.write(f" {SETTINGS['VELOCITY_BACKEND']}:\n") - spec_file.write(f" {SETTINGS['VELOCITY_DISTRO']}: {{}}\n") - with open(image_dir.joinpath('templates', f"{SETTINGS['VELOCITY_DISTRO']}.vtmp"), 'x') as temp_file: - temp_file.write('\n') - temp_file.write('@from\n') - temp_file.write(' %(__base__)\n') - temp_file.write('\n') - temp_file.write('@label\n') - temp_file.write(' velocity.image.%(__name__)__%(__tag__) %(__hash__)\n') - - else: - parser.print_help() - print() # add newline \ No newline at end of file + bare_print( + [ + TextBlock("^", fore=Fore.GREEN, style=Style.BRIGHT), + TextBlock(spec, fore=Fore.MAGENTA, style=Style.BRIGHT), + ] + ) + if len(fdt[seed]) > 0: + for un in fdt[seed]: + spec_print(un, (indent + 1), fdt, rs) + + # print specs + for tl in top_level_entries: + spec_print(tl.name, 0, flat_dep_tree, recipe) + print() # add newline +else: + parser.print_help() + print() # add newline diff --git a/src/velocity/_backends.py b/src/velocity/_backends.py index 8a71932..e62174d 100644 --- a/src/velocity/_backends.py +++ b/src/velocity/_backends.py @@ -94,7 +94,7 @@ def _filter_content(cls, image: Image, text: str) -> str: text = "" # remove comments, newlines, and superfluous white space - text = re_sub(r"###.*", "", text) + text = re_sub(r">>>.*", "", text) text = re_sub(r"\n", "", text) text = re_sub(r"^\s+|\s+$", "", text) @@ -121,6 +121,7 @@ def __init__(self, name: str, executable: str) -> None: def generate_script(self, image: Image, variables: dict[str, str]) -> list[str]: """Generate a build script e.g. .dockerfile/.def""" + logger.debug("Variables: {}".format(variables)) template: list[str] = self._load_template(image, variables) sections: dict[str, list[str]] = self._get_sections(template) script: list[str] = list() @@ -149,10 +150,11 @@ def generate_script(self, image: Image, variables: dict[str, str]) -> list[str]: if "@run" in sections: env_ext: list[str] = list() script.extend(self._run(sections["@run"], env_ext)) - if "@env" in sections: - sections["@env"].extend(env_ext) - else: - sections["@env"] = env_ext + if len(env_ext) > 0: + if "@env" in sections: + sections["@env"].extend(env_ext) + else: + sections["@env"] = env_ext # @env if "@env" in sections: script.extend(self._env(sections["@env"])) @@ -224,9 +226,14 @@ def format_image_name(self, path: Path, tag: str) -> str: def clean_up_old_image_tag(self, name: str) -> str: """Generate CLI command to clean up an old image.""" + @abstractmethod def build_exists(self, name: str) -> bool: """Check if an images has been built.""" + @abstractmethod + def generate_final_image_cmd(self, src: str, dest: str) -> str: + """Generate command to move the last image in the build to its final destination.""" + class Podman(Backend): """Podman backend.""" @@ -348,11 +355,14 @@ def format_image_name(self, path: Path, tag: str) -> str: return "{}{}{}".format("localhost/" if "/" not in tag else "", tag, ":latest" if ":" not in tag else "") def clean_up_old_image_tag(self, name: str) -> str: - return "podman untag {}".format(name) + return "podman rmi {}".format(name) def build_exists(self, name: str) -> bool: return False + def generate_final_image_cmd(self, src: str, dest: str) -> str: + return "{} tag {} {}".format(self.executable, src, dest) + class Apptainer(Backend): """Apptainer backend.""" @@ -391,7 +401,7 @@ def _copy(self, contents: list[str]) -> list[str]: for ln in contents: if len(ln.split()) != 2: raise TemplateSyntaxError("Your '@copy' can only have one source and destination!", ln) - ret.append(" {}".format(ln)) + ret.append("{}".format(ln)) return ret def _run(self, contents: list[str], label_contents: list[str]) -> list[str]: @@ -402,25 +412,25 @@ def _run(self, contents: list[str], label_contents: list[str]) -> list[str]: res = re_match(r"^!envar\s+(?P\S+)\s+(?P.*)$", cmd) cmd = 'export {name}="{value}"'.format(**res.groupdict()) label_contents.append("{name} {value}".format(**res.groupdict())) - ret.append(" {}".format(cmd)) + ret.append("{}".format(cmd)) return ret def _env(self, contents: list[str]) -> list[str]: ret: list[str] = ["", "%environment"] for env in contents: parts = env.split() - ret.append(' export {}="{}"'.format(parts[0], env.lstrip(parts[0]).strip(" "))) + ret.append('export {}="{}"'.format(parts[0], env.lstrip(parts[0]).strip(" "))) return ret def _label(self, contents: list[str]) -> list[str]: ret: list[str] = ["", "%labels"] for label in contents: parts = label.split() - ret.append(" {} {}".format(parts[0], label.lstrip(parts[0]).strip(" "))) + ret.append("{} {}".format(parts[0], label.lstrip(parts[0]).strip(" "))) return ret def _entry(self, contents: list[str]) -> list[str]: - return ["", "%runscript", " {}".format(contents[0])] + return ["", "%runscript", "{}".format(contents[0])] def generate_build_cmd(self, src: str, dest: str, args: list = None) -> str: cmd: list[str] = ["{} build".format(self.executable)] @@ -444,6 +454,9 @@ def build_exists(self, name: str) -> bool: return True return False + def generate_final_image_cmd(self, src: str, dest: str) -> str: + return "cp {} {}".format(src, dest) + def get_backend() -> Backend: backend = config.get("velocity:backend") diff --git a/src/velocity/_build.py b/src/velocity/_build.py index 458d114..27d7c41 100644 --- a/src/velocity/_build.py +++ b/src/velocity/_build.py @@ -9,6 +9,7 @@ from threading import Thread from pathlib import Path from colorama import Fore, Style +from platform import processor as arch from ._config import config from ._graph import Image from ._print import header_print, indent_print, TextBlock @@ -97,6 +98,10 @@ def __init__( self.build_dir = Path(config.get("velocity:build_dir")) self.build_dir.mkdir(mode=0o777, parents=True, exist_ok=True) + self.variables: dict[str, str] = dict() + for i in self.build_units: + self.variables["__{}__version__".format(i.name)] = i.version.__str__() + def build(self) -> None: """Launch image builds.""" # store pwd @@ -110,28 +115,34 @@ def build(self) -> None: else: entry.unlink() - last = None # last image that was built + last = str() # last image that was built + build_names: list[str] = list() for u in self.build_units: - if u == self.build_units[-1]: - tag = str( - self.build_name - if self.build_name is not None - else "{}__{}-{}".format( - "_".join(f"{bu.name}-{bu.version}" for bu in reversed(self.build_units)), - config.get("velocity:system"), - config.get("velocity:distro"), - ) - ) - name = self.backend_engine.format_image_name(Path(pwd.absolute()), tag) - else: - name = self.backend_engine.format_image_name( - Path.joinpath(self.build_dir, "{}-{}-{}".format(u.name, u.version, u.id)), - u.id - ) + name = self.backend_engine.format_image_name( + Path.joinpath(self.build_dir, "{}-{}-{}".format(u.name, u.version, u.id)), u.id + ) self._build_image(u, last, name) - if not self.dry_run and self.remove_tags and last is not None: - run(self.backend_engine.clean_up_old_image_tag(last)) last = name + build_names.append(name) + + tag = str( + self.build_name + if self.build_name is not None + else "{}__{}-{}".format( + "_".join(f"{bu.name}-{bu.version}" for bu in reversed(self.build_units)), + config.get("velocity:system"), + config.get("velocity:distro"), + ) + ) + + final_name = self.backend_engine.format_image_name(Path(pwd.absolute()), tag) + if not self.dry_run: + run(self.backend_engine.generate_final_image_cmd(last, final_name)) + header_print([TextBlock("BUILT: "), TextBlock(final_name, fore=Fore.MAGENTA, style=Style.BRIGHT)]) + + if not self.dry_run and self.remove_tags: + for bn in build_names: + run(self.backend_engine.clean_up_old_image_tag(bn)) # go back to the starting dir os.chdir(pwd) @@ -196,8 +207,10 @@ def _build_image(self, unit: Image, src_image: str, name: str): script_variables.update( {"__threads__": str(int(os.cpu_count() * 0.75) if int(os.cpu_count() * 0.75) < 16 else 16)} ) + script_variables.update({"__arch__": arch()}) if src_image is not None: script_variables.update({"__base__": src_image}) + script_variables.update(self.variables) script = self.backend_engine.generate_script(unit, script_variables) # write out script diff --git a/src/velocity/_config.py b/src/velocity/_config.py index 8ced3c8..87101fa 100644 --- a/src/velocity/_config.py +++ b/src/velocity/_config.py @@ -3,8 +3,8 @@ from loguru import logger from platform import processor as arch from pathlib import Path -from os import getlogin as get_username -from importlib.metadata import version +from os import getlogin as get_username, getenv +from yaml import safe_load as yaml_safe_load from ._exceptions import InvalidConfigIdentifier @@ -24,9 +24,7 @@ def set(self, item: str, value: int | bool | str | list | dict | None) -> None: for p in parts: # make all config identifiers comply with python identifiers if not p.isidentifier(): - raise InvalidConfigIdentifier( - "'{}' is not a valid identifier.".format(item) - ) + raise InvalidConfigIdentifier("'{}' is not a valid identifier.".format(item)) # walk config tree until the final node is found if p != parts[-1]: if p not in set_value: @@ -48,9 +46,7 @@ def get(self, item: str) -> int | bool | str | list | dict | None: # make all config identifiers comply with python identifiers for p in parts: if not p.isidentifier(): - raise InvalidConfigIdentifier( - "'{}' is not a valid identifier.".format(item) - ) + raise InvalidConfigIdentifier("'{}' is not a valid identifier.".format(item)) ret_value = ret_value[p] return ret_value else: @@ -61,24 +57,66 @@ def get(self, item: str) -> int | bool | str | list | dict | None: logger.exception(e) return None + def load(self) -> None: + """Load configuration.""" + if self.get("velocity:config_dir") is None: + self.set("velocity:config_dir", Path.home().joinpath(".velocity").__str__()) + config_dir = Path(self.get("velocity:config_dir")) + try: + with open(config_dir.joinpath("config.yaml"), "r") as fi: + conf = yaml_safe_load(fi) + for k in conf: + self.set(k, conf[k]) + except FileNotFoundError: + logger.warning("Could not load configuration file from '{}'!".format(config_dir.joinpath("config.yaml"))) + def __str__(self) -> str: return str(self._config) # default configuration & singleton _config = Config() -_config.set( - "velocity", - { - "system": arch(), - "backend": "apptainer", - "distro": "ubuntu", - "verbose": False, - "debug": "WARNING", - "config_dir": Path.home().joinpath(".velocity", "config"), - "image_path": Path.home().joinpath(".velocity", "images"), - "build_dir": Path("/tmp").joinpath(get_username(), "velocity"), - "version": version("velocity") - }, -) +_config.load() + +# get config from environment variables +if getenv("VELOCITY_SYSTEM") is not None: + _config.set("velocity:system", getenv("VELOCITY_SYSTEM")) + +if getenv("VELOCITY_BACKEND") is not None: + _config.set("velocity:backend", getenv("VELOCITY_BACKEND")) + +if getenv("VELOCITY_DISTRO") is not None: + _config.set("velocity:distro", getenv("VELOCITY_DISTRO")) + +if getenv("VELOCITY_CONFIG_DIR") is not None: + _config.set("velocity:config_dir", getenv("VELOCITY_CONFIG_DIR")) + +if getenv("VELOCITY_IMAGE_PATH") is not None: + _config.set("velocity:image_path", getenv("VELOCITY_IMAGE_PATH")) + +if getenv("VELOCITY_BUILD_DIR") is not None: + _config.set("velocity:build_dir", getenv("VELOCITY_BUILD_DIR")) + +# set defaults for un-configured items +if _config.get("velocity:system") is None: + _config.set("velocity:system", arch()) + +if _config.get("velocity:backend") is None: + _config.set("velocity:backend", "apptainer") + +if _config.get("velocity:distro") is None: + _config.set("velocity:distro", "ubuntu") + +if _config.get("velocity:debug") is None: + _config.set("velocity:debug", "WARNING") + +if _config.get("velocity:image_path") is None: + image_dir = Path.home().joinpath(".velocity", "images") + image_dir.mkdir(parents=True, exist_ok=True) + _config.set("velocity:image_path", image_dir.__str__()) + +if _config.get("velocity:build_dir") is None: + _config.set("velocity:build_dir", Path("/tmp").joinpath(get_username(), "velocity").__str__()) + +# export config = _config diff --git a/src/velocity/_exceptions.py b/src/velocity/_exceptions.py index 472e227..1e69771 100644 --- a/src/velocity/_exceptions.py +++ b/src/velocity/_exceptions.py @@ -1,69 +1,78 @@ +"""Exceptions for velocity.""" + from sys import stderr -class InvalidDependencySpecification(Exception): +class CannotFindDependency(Exception): + """Cannot find an image dependency.""" - def __init__(self, spec, image, tag, file): - super().__init__(f"The provided dependency '{spec}' for {image}@={tag} is invalid! Please fix {file}") + def __init__(self, *args) -> None: + super().__init__(*args) class NoAvailableBuild(Exception): + """No available build can be found.""" - def __init__(self, message): - super().__init__(message) + def __init__(self, *args) -> None: + super().__init__(*args) class EdgeViolatesDAG(Exception): + """Edge breaks the DAG requirement of a graph.""" - def __init__(self, u_of_edge, v_of_edge, cycle): + def __init__(self, u_of_edge, v_of_edge, cycle) -> None: super().__init__(f"Addition of edge {u_of_edge} -> {v_of_edge} violates graph DAG requirement!") for c in cycle: - print(f'{c[0]} -> {c[1]}', file=stderr) + print(f"{c[0]} -> {c[1]}", file=stderr) class BackendNotSupported(Exception): + """Container backend not supported.""" + + def __init__(self, *args): + super().__init__(*args) - def __init__(self, backend): - super().__init__(f"The '{backend}' is not supported!") + +class BackendNotAvailable(Exception): + """Container backend not supported.""" + + def __init__(self, *args): + super().__init__(*args) class UndefinedVariableInTemplate(Exception): + """Undefined variable during substitution in template.""" - def __init__(self, variable): - super().__init__(f"The variable '{variable}' is undefined!") + def __init__(self, *args): + super().__init__(*args) class RepeatedSection(Exception): + """Repeated template section.""" - def __init__(self, section): - super().__init__(f"You have more than one '{section}' section in your template!") + def __init__(self, *args): + super().__init__(*args) class LineOutsideOfSection(Exception): - - def __init__(self): - super().__init__(f"You have a line outside of a section in your template!") + def __init__(self, *args): + super().__init__(*args) class TemplateSyntaxError(Exception): - def __init__(self, message, line: str = None): super().__init__(f"{message} {f':line: <{line}>' if line is not None else ''}") class InvalidImageVersionError(Exception): """Invalid image version spec.""" - def __init__(self, *args): - super().__init__(*args) - -class InvalidSchema(Exception): - """Invalid schema.""" def __init__(self, *args): super().__init__(*args) -class InvalidConfig(Exception): - """Invalid config.""" - def __init__(self, *args): +class InvalidConfigIdentifier(Exception): + """Invalid configuration identifier.""" + + def __init__(self, *args) -> None: super().__init__(*args) diff --git a/src/velocity/_graph.py b/src/velocity/_graph.py index bc513dd..d0c1022 100644 --- a/src/velocity/_graph.py +++ b/src/velocity/_graph.py @@ -54,7 +54,6 @@ def __init__(self, version_specifier: str) -> None: self.minor: int | None = int(version_dict["minor"]) if version_dict["minor"] is not None else None self.patch: int | None = int(version_dict["patch"]) if version_dict["patch"] is not None else None self.suffix: str | None = str(version_dict["suffix"]) if version_dict["suffix"] is not None else None - logger.trace("Version: {}".format(self.__str__())) except AttributeError: self.major: int | None = None self.minor: int | None = None @@ -260,7 +259,7 @@ def apply_constraint(self, conditional: str, _type: str, spec: str) -> bool: self.arguments.add(spec) elif _type == "template": self.template = spec - elif self.files == "file": + elif _type == "file": self.files.add(spec) elif _type == "prolog": self.prolog = spec @@ -274,7 +273,7 @@ def hash(self) -> str: hash_list.append(self.name) hash_list.append(self.version) hash_list.append(self.system) - hash_list.append(self.backend) + # hash_list.append(self.backend) # disable backend for now because it should not make a difference in the image hash_list.append(self.distro) hash_list.append(",".join(str(x) for x in self.dependencies)) hash_list.append(",".join(str(x) for x in self.variables)) @@ -520,9 +519,41 @@ class ImageRepo: """Image repository.""" def __init__(self) -> None: + self.images: set[Image] = set() # constraint(image, condition, type, spec, scope) self.constraints: list[tuple[str, str, str, str, str]] = list() - self.images: set[Image] = set() + if config.get("constraints") is not None: + # arguments + if "arguments" in config.get("constraints"): + for argument in config.get("constraints")["arguments"]: + if isinstance(argument["value"], list): + specs = argument["value"] + else: + specs = [ + argument["value"], + ] + for spec in specs: + self.constraints.append( + ( + "", + argument["when"] if "when" in argument else "", + "argument", + spec, + "global", + ) + ) + # variables + if "variables" in config.get("constraints"): + for variable in config.get("constraints")["variables"]: + self.constraints.append( + ( + "", + variable["when"] if "when" in variable else "", + "variable", + "{}={}".format(variable["name"], variable["value"]), + "global", + ) + ) def import_from_dir(self, path: str) -> None: """Add Images from path.""" @@ -532,63 +563,90 @@ def import_from_dir(self, path: str) -> None: for name in [x for x in p.iterdir() if x.is_dir() and x.name[0] != "."]: with open(name.joinpath("specs.yaml"), "r") as fi: - specs = yaml_safe_load(fi) + specs_file = yaml_safe_load(fi) # add versions - for version in specs["versions"]: - image = Image( - name.name, - version["spec"], - config.get("velocity:system"), - config.get("velocity:backend"), - config.get("velocity:distro"), - str(name), - ) - if "when" in version: - if image.satisfies(version["when"]): - self.images.add(image) + for version in specs_file["versions"]: + if isinstance(version["spec"], list): + specs = version["spec"] else: - self.images.add(image) - + specs = [ + version["spec"], + ] + for spec in specs: + image = Image( + name.name, + spec, + config.get("velocity:system"), + config.get("velocity:backend"), + config.get("velocity:distro"), + str(name), + ) + if "when" in version: + if image.satisfies(version["when"]): + self.images.add(image) + else: + self.images.add(image) # add constraints # dependencies - if "dependencies" in specs: - for dependency in specs["dependencies"]: - self.constraints.append( - ( - name.name, - dependency["when"] if "when" in dependency else "", - "dependency", + if "dependencies" in specs_file: + for dependency in specs_file["dependencies"]: + if isinstance(dependency["spec"], list): + specs = dependency["spec"] + else: + specs = [ dependency["spec"], - dependency["scope"] if "scope" in dependency else "image", + ] + for spec in specs: + self.constraints.append( + ( + name.name, + dependency["when"] if "when" in dependency else "", + "dependency", + spec, + dependency["scope"] if "scope" in dependency else "image", + ) ) - ) # templates - if "templates" in specs: - for template in specs["templates"]: - self.constraints.append( - ( - name.name, - template["when"] if "when" in template else "", - "template", - template["path"], - template["scope"] if "scope" in template else "image", + if "templates" in specs_file: + for template in specs_file["templates"]: + if isinstance(template["name"], list): + specs = template["name"] + else: + specs = [ + template["name"], + ] + for spec in specs: + self.constraints.append( + ( + name.name, + template["when"] if "when" in template else "", + "template", + spec, + template["scope"] if "scope" in template else "image", + ) ) - ) # arguments - if "arguments" in specs: - for argument in specs["arguments"]: - self.constraints.append( - ( - name.name, - argument["when"] if "when" in argument else "", - "argument", + if "arguments" in specs_file: + for argument in specs_file["arguments"]: + if isinstance(argument["value"], list): + specs = argument["value"] + else: + specs = [ argument["value"], - argument["scope"] if "scope" in argument else "image", + ] + for spec in specs: + self.constraints.append( + ( + name.name, + argument["when"] if "when" in argument else "", + "argument", + spec, + argument["scope"] if "scope" in argument else "image", + ) ) - ) # variables - if "variables" in specs: - for variable in specs["variables"]: + if "variables" in specs_file: + for variable in specs_file["variables"]: self.constraints.append( ( name.name, @@ -599,20 +657,27 @@ def import_from_dir(self, path: str) -> None: ) ) # files - if "files" in specs: - for file in specs["files"]: - self.constraints.append( - ( - name.name, - file["when"] if "when" in file else "", - "file", + if "files" in specs_file: + for file in specs_file["files"]: + if isinstance(file["name"], list): + specs = file["name"] + else: + specs = [ file["name"], - file["scope"] if "scope" in file else "image", + ] + for spec in specs: + self.constraints.append( + ( + name.name, + file["when"] if "when" in file else "", + "file", + spec, + file["scope"] if "scope" in file else "image", + ) ) - ) # prologs - if "prologs" in specs: - for prolog in specs["prologs"]: + if "prologs" in specs_file: + for prolog in specs_file["prologs"]: self.constraints.append( ( name.name, @@ -623,12 +688,11 @@ def import_from_dir(self, path: str) -> None: ) ) - def create_build_recipe(self, targets: list[str]) -> tuple: + def create_build_recipe(self, targets: list[str]) -> tuple[tuple, ImageGraph]: """Create an ordered build recipe of images.""" images: set[Image] = deepcopy(self.images) build_targets: list[Target] = list() - targs: list[Image] = list() for target in targets: res = re_fullmatch( r"^(?P[a-zA-Z0-9-]+)(?:(?:@(?P[\d\.]+)(?!@))?(?:@?(?P:)(?P[\d\.]+)?)?)?$", @@ -646,7 +710,6 @@ def create_build_recipe(self, targets: list[str]) -> tuple: config.get("velocity:distro"), "", ) - targs.append(t) if gd["colen"] is not None: build_targets.append(Target(t, DepOp.GE)) else: @@ -663,7 +726,6 @@ def create_build_recipe(self, targets: list[str]) -> tuple: "", ) build_targets.append(Target(t, DepOp.LE)) - targs.append(t) else: raise InvalidImageVersionError("Invalid version '{}'.".format(target)) # n @@ -680,7 +742,6 @@ def create_build_recipe(self, targets: list[str]) -> tuple: "", ) build_targets.append(Target(t, DepOp.UN)) - targs.append(t) # n@v:v else: t = Image( @@ -692,7 +753,6 @@ def create_build_recipe(self, targets: list[str]) -> tuple: "", ) build_targets.append(Target(t, DepOp.GE)) - targs.append(t) t = Image( gd["name"], gd["right"], @@ -702,22 +762,38 @@ def create_build_recipe(self, targets: list[str]) -> tuple: "", ) build_targets.append(Target(t, DepOp.LE)) - targs.append(t) else: raise NoAvailableBuild("No available build!") - # apply constraints + # pre-burner graph + for constraint in self.constraints: + pass + for image in images: + if image.apply_constraint("{} {}".format(constraint[0], constraint[1]), constraint[2], constraint[3]): + images_changed = True + ig = ImageGraph() + for image in images: + ig.add_node(image) + for image in images: + for dep in image.dependencies: + for di in images: + if di.satisfies(dep): + ig.add_edge(image, di) + + bt: tuple[Image] = ig.create_build_recipe(build_targets) + + # apply constraints for the build scope images_changed: bool = True while images_changed: images_changed = False for constraint in self.constraints: if constraint[4] == "build": - for targ in targs: + for targ in bt: if targ.satisfies(constraint[1]): for image in images: if image.apply_constraint(constraint[0], constraint[2], constraint[3]): images_changed = True - # default is constraint[4] == "image" + # "image" or "universal" else: for image in images: if image.apply_constraint( @@ -743,4 +819,13 @@ def create_build_recipe(self, targets: list[str]) -> tuple: b.underlay = cumulative_deps cumulative_deps = cumulative_deps + int(b.id, 16) - return bt + bt_ig = ImageGraph() + for image in bt: + bt_ig.add_node(image) + for image in bt: + for dep in image.dependencies: + for di in bt: + if di.satisfies(dep): + bt_ig.add_edge(image, di) + + return bt, bt_ig From 573a7d089ef59e31742bf4b74d49397ba8e4bdf3 Mon Sep 17 00:00:00 2001 From: Asa Rentschler Date: Mon, 16 Sep 2024 11:27:04 -0400 Subject: [PATCH 06/10] Added more variables for different parts of the version --- src/velocity/__main__.py | 8 +++++++- src/velocity/_build.py | 8 ++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/velocity/__main__.py b/src/velocity/__main__.py index 0c82b96..39e1625 100644 --- a/src/velocity/__main__.py +++ b/src/velocity/__main__.py @@ -44,6 +44,7 @@ "-l", "--leave-tags", action="store_true", help="do not clean up intermediate build tags (only applies to podman)" ) build_parser.add_argument("-v", "--verbose", action="store_true", help="print helpful debug/runtime information") +build_parser.add_argument("-c", "--clean", action="store_true", help="run clean build (delete cached builds)") # create avail_parser avail_parser = sub_parsers.add_parser("avail", help="lookup available images") @@ -99,7 +100,12 @@ # prep builder builder = ImageBuilder( - recipe, build_name=args.name, dry_run=args.dry_run, remove_tags=not args.leave_tags, verbose=args.verbose + recipe, + build_name=args.name, + dry_run=args.dry_run, + remove_tags=not args.leave_tags, + verbose=args.verbose, + clean_build_dir=args.clean ) # build diff --git a/src/velocity/_build.py b/src/velocity/_build.py index 27d7c41..95fd944 100644 --- a/src/velocity/_build.py +++ b/src/velocity/_build.py @@ -101,6 +101,10 @@ def __init__( self.variables: dict[str, str] = dict() for i in self.build_units: self.variables["__{}__version__".format(i.name)] = i.version.__str__() + self.variables["__{}__version_major__".format(i.name)] = i.version.major.__str__() + self.variables["__{}__version_minor__".format(i.name)] = i.version.minor.__str__() + self.variables["__{}__version_patch__".format(i.name)] = i.version.patch.__str__() + self.variables["__{}__version_suffix__".format(i.name)] = i.version.suffix.__str__() def build(self) -> None: """Launch image builds.""" @@ -203,6 +207,10 @@ def _build_image(self, unit: Image, src_image: str, name: str): script_variables = unit.variables.copy() script_variables.update({"__name__": unit.name}) script_variables.update({"__version__": str(unit.version)}) + script_variables.update({"__version_major__": str(unit.version.major)}) + script_variables.update({"__version_minor__": str(unit.version.minor)}) + script_variables.update({"__version_patch__": str(unit.version.patch)}) + script_variables.update({"__version_suffix__": str(unit.version.suffix)}) script_variables.update({"__timestamp__": str(datetime.datetime.now())}) script_variables.update( {"__threads__": str(int(os.cpu_count() * 0.75) if int(os.cpu_count() * 0.75) < 16 else 16)} From 8b66771e71b306a0299a11b1128bcebfbf05b92b Mon Sep 17 00:00:00 2001 From: Asa Rentschler Date: Mon, 16 Sep 2024 16:14:54 -0400 Subject: [PATCH 07/10] Various bug fixes --- pyproject.toml | 6 +++--- src/velocity/__main__.py | 7 +++---- src/velocity/_graph.py | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4727370..89903c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = [ "setuptools>=61.0" ] build-backend = "setuptools.build_meta" [project] -name = "velocity" +name = "olcf-velocity" version = "0.1.0-rc1" authors = [ { name="Asa"} @@ -14,8 +14,8 @@ dependencies = [ "pyyaml", "networkx", "colorama", - "editor", - "loguru" + "loguru", + "typing_extensions" ] [project.urls] diff --git a/src/velocity/__main__.py b/src/velocity/__main__.py index 39e1625..6f27cae 100644 --- a/src/velocity/__main__.py +++ b/src/velocity/__main__.py @@ -2,11 +2,10 @@ import argparse import sys -from docutils.parsers.rst.directives.images import Image from loguru import logger from importlib.metadata import version from colorama import Fore, Style -from ._graph import ImageRepo +from ._graph import ImageRepo, Image from ._build import ImageBuilder from ._print import header_print, indent_print, TextBlock, bare_print from ._config import config @@ -95,7 +94,7 @@ # print build specs header_print([TextBlock("Build Order:")]) for r in recipe: - indent_print([TextBlock(f"{r.name}@{r.version}", fore=Fore.MAGENTA, style=Style.BRIGHT)]) + indent_print([TextBlock(f"{r.name}@{r.version}-{r.id}", fore=Fore.MAGENTA, style=Style.BRIGHT)]) print() # newline # prep builder @@ -157,7 +156,7 @@ def spec_print(seed: str, indent: int, fdt: dict, rs: tuple[Image]): spec = "" for _ in rs: if _.satisfies(seed): - spec = "{}@{}".format(_.name, _.version) + spec = "{}@{}-{}".format(_.name, _.version, _.id) print(" " + " " * indent, end="") if indent == 0: bare_print( diff --git a/src/velocity/_graph.py b/src/velocity/_graph.py index d0c1022..b5fd85f 100644 --- a/src/velocity/_graph.py +++ b/src/velocity/_graph.py @@ -278,7 +278,7 @@ def hash(self) -> str: hash_list.append(",".join(str(x) for x in self.dependencies)) hash_list.append(",".join(str(x) for x in self.variables)) hash_list.append(",".join(str(x) for x in self.arguments)) - tf = Path(self.path).joinpath("templates", self.template) + tf = Path(self.path).joinpath("templates", "{}.vtmp".format(self.template)) if tf.is_file(): hash_list.append(sha256(tf.read_bytes()).hexdigest()) else: From ceb1d8458930bd5c94fcb8eab693fa31b90ca1f8 Mon Sep 17 00:00:00 2001 From: Asa <59259908+AcerP-py@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:42:16 -0400 Subject: [PATCH 08/10] Create python-publish.yml --- .github/workflows/python-publish.yml | 39 ++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/python-publish.yml diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 0000000..b7a704b --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,39 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Upload Python Package + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: python -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} From b52eb80bba3d3c83826eb9b8375492afe0746822 Mon Sep 17 00:00:00 2001 From: Asa Rentschler Date: Wed, 18 Sep 2024 14:49:08 -0400 Subject: [PATCH 09/10] Commit updated docs --- docs/build/doctrees/environment.pickle | Bin 37024 -> 39605 bytes docs/build/doctrees/index.doctree | Bin 3566 -> 3991 bytes docs/build/doctrees/reference/build.doctree | Bin 9122 -> 8700 bytes docs/build/doctrees/reference/config.doctree | Bin 0 -> 11050 bytes docs/build/doctrees/reference/index.doctree | Bin 2855 -> 2885 bytes .../doctrees/reference/specifications.doctree | Bin 17579 -> 0 bytes docs/build/doctrees/reference/specs.doctree | Bin 0 -> 15033 bytes docs/build/doctrees/reference/vtmp.doctree | Bin 19027 -> 21133 bytes docs/build/doctrees/starting/basic.doctree | Bin 24659 -> 17083 bytes docs/build/doctrees/starting/index.doctree | Bin 2827 -> 2842 bytes docs/build/doctrees/starting/tutorial.doctree | Bin 120466 -> 60169 bytes docs/build/html/_sources/index.rst.txt | 6 +- .../html/_sources/reference/build.rst.txt | 24 +- .../html/_sources/reference/config.rst.txt | 64 + .../html/_sources/reference/index.rst.txt | 3 +- .../_sources/reference/specifications.rst.txt | 160 --- .../html/_sources/reference/specs.rst.txt | 114 ++ .../html/_sources/reference/vtmp.rst.txt | 97 +- .../html/_sources/starting/basic.rst.txt | 204 +-- .../html/_sources/starting/tutorial.rst.txt | 1241 ++++++----------- docs/build/html/index.html | 13 +- docs/build/html/objects.inv | Bin 462 -> 493 bytes docs/build/html/reference/build.html | 30 +- docs/build/html/reference/config.html | 191 +++ docs/build/html/reference/index.html | 23 +- docs/build/html/reference/specifications.html | 278 ---- docs/build/html/reference/specs.html | 234 ++++ docs/build/html/reference/vtmp.html | 114 +- docs/build/html/searchindex.js | 2 +- docs/build/html/starting/basic.html | 207 +-- docs/build/html/starting/index.html | 7 +- docs/build/html/starting/tutorial.html | 1229 ++++++---------- docs/source/_static/icon.png | 2 +- docs/source/index.rst | 6 +- docs/source/reference/build.rst | 24 +- docs/source/reference/config.rst | 64 + docs/source/reference/index.rst | 3 +- docs/source/reference/specifications.rst | 160 --- docs/source/reference/specs.rst | 114 ++ docs/source/reference/vtmp.rst | 97 +- docs/source/starting/basic.rst | 204 +-- docs/source/starting/tutorial.rst | 1241 ++++++----------- src/velocity/__main__.py | 2 +- 43 files changed, 2459 insertions(+), 3699 deletions(-) create mode 100644 docs/build/doctrees/reference/config.doctree delete mode 100644 docs/build/doctrees/reference/specifications.doctree create mode 100644 docs/build/doctrees/reference/specs.doctree create mode 100644 docs/build/html/_sources/reference/config.rst.txt delete mode 100644 docs/build/html/_sources/reference/specifications.rst.txt create mode 100644 docs/build/html/_sources/reference/specs.rst.txt create mode 100644 docs/build/html/reference/config.html delete mode 100644 docs/build/html/reference/specifications.html create mode 100644 docs/build/html/reference/specs.html create mode 100644 docs/source/reference/config.rst delete mode 100644 docs/source/reference/specifications.rst create mode 100644 docs/source/reference/specs.rst diff --git a/docs/build/doctrees/environment.pickle b/docs/build/doctrees/environment.pickle index d5d0f20fd28eebe359deb665ec25deaad7bd2633..d75c8d143dbb68e8fb77b966824193054646817b 100644 GIT binary patch literal 39605 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 RmyeXA-ufin*l=Rx{{TjVTi*Zx diff --git a/docs/build/doctrees/index.doctree b/docs/build/doctrees/index.doctree index 208a8f3f89c04ab0d89496b1b5110db515fbab7d..58d112fe40e9f6be1ca5424cb4582504489699b9 100644 GIT binary patch delta 743 zcmb_aL2DC16n4AWrl~C&3zenSKB=vVl=k4slXx|?tzdc!ak4wf+je(m*_o{kRH}%O zz(O!HA|Ct^k{{56SAT;4KoG&3C&$F4AP62jym|1wZ{B+|-}iOt?TYiayG_i&vA8PZ=aT8l8{b9V$@1kU+8DLf=)Krry=(O*@Zv= z0m*r24nP{l@HCJJ5-CI^V3IPzJPN!G9%s@ZG=Wwcggyu_31TULGl1>5=b|B_{L>TY{NE}bPRDm!b{6NTri_QEE|9i3Rzi#%gi@nm5Pg}(M zs%PIW-@{v2!D_99i@1U7XseI*YpbQ|g@gIWziV@*_DFpy+_|}%vnG|Dw1s=Q3OGF$ zuCBh>)dDv0iQ04Aecx#Ll3=?wg`FM5J_da}=;LAS0)~Bz8al-_%mC9O!&L%%vmphpAD0u(Wiw7_ zJ2=*vwWZbd;)5%_Y+g`LvNUQQw|Nrj-sq$VmUT9IKl6Rc>Z#$yFUvz5h^)j&L}fRo S!Z;C!eSxmYgVDRWy7d!?{0+JQ delta 370 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 yPgp*)z>LcSOP1myY#6be#&-ZNikZjzh3HANcO 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 ANB{r; literal 0 HcmV?d00001 diff --git a/docs/build/doctrees/reference/index.doctree b/docs/build/doctrees/reference/index.doctree index 9c87a5668254e8e9440d4960f04b83fd62763f88..86d4366e57f480e5d0f52b41970b1bb369fcd649 100644 GIT binary patch delta 158 zcmZ23c2ta|fpx0YMiyQs5rLxAwA7;1yyR5<iShFzB#H}ClPB9U z88KzJZO&(U#VAmepI;JR0924!F{Ou5f6C-=7B8WW9^T^Aqwp@&*fpx0PMiyQs9loN}wA7;1yyR5> A=>Px# diff --git a/docs/build/doctrees/reference/specifications.doctree b/docs/build/doctrees/reference/specifications.doctree deleted file mode 100644 index 8daa0cb39747c6800babfbdedf2dd5879f984572..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17579 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 C6}xZ% literal 0 HcmV?d00001 diff --git a/docs/build/doctrees/reference/vtmp.doctree b/docs/build/doctrees/reference/vtmp.doctree index 0f868aa56a82cffebc268d52d70b871a17837748..472c86d686e7aa1ba76225f6e5b9d5c944efa382 100644 GIT binary patch literal 21133 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 delta 5305 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 literal 24659 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 diff --git a/docs/build/doctrees/starting/index.doctree b/docs/build/doctrees/starting/index.doctree index 8123b84d9d08416d32613123a383131a7fc12912..4e355b75b5f6d18edf32d864c523842ee4a1b7ee 100644 GIT binary patch delta 72 zcmeAcnPyo5S>c#Bh$OY%x{<15ng bi*l!QY@W%Yz{uFKc^B(vCLpz*BZdh8ht?Ur delta 58 zcmbOw)-A@;z&e#-Ba0==WPcV95pTvG-s05clDyK~_=>drqTDIojGI@oC@?a%Z9c{N NnTZL=-NzBb1OO1k5|;n~ diff --git a/docs/build/doctrees/starting/tutorial.doctree b/docs/build/doctrees/starting/tutorial.doctree index 11b68cc10ae460be0f53398d8456af91781280d1..b67ba94d6804bdcbd951fa9b233e2ec71f55b63d 100644 GIT binary patch literal 60169 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 S`zVAvV>H>^y0O_z_5TC7!Fz%L diff --git a/docs/build/html/_sources/index.rst.txt b/docs/build/html/_sources/index.rst.txt index e81dc9e..fcb521a 100644 --- a/docs/build/html/_sources/index.rst.txt +++ b/docs/build/html/_sources/index.rst.txt @@ -4,8 +4,10 @@ contain the root `toctree` directive. -Velocity Container Build System -=============================== +Velocity Container Build Management +=================================== +Velocity is a tool to help with the maintenance of container build scripts on multiple systems, backends +(e.g podman or apptainer) and distros. .. toctree:: :maxdepth: 2 diff --git a/docs/build/html/_sources/reference/build.rst.txt b/docs/build/html/_sources/reference/build.rst.txt index 4d4d7f1..6003d21 100644 --- a/docs/build/html/_sources/reference/build.rst.txt +++ b/docs/build/html/_sources/reference/build.rst.txt @@ -6,16 +6,14 @@ This page describes the steps that velocity goes through when building an image. Setup ##### -After selecting images to build and ordering them. Velocity will start by assigning a random -id to each image to be built. It will also create a directory BSD (Build Sub Dir) named with that random id in -`VELOCITY_BUILD_DIR`. +After selecting images to build and ordering them Velocity will also create a directory BSD (Build Sub Dir) +named ``--`` in build directory. .. _files: Files ##### -Next Velocity will look for a folder in the image definition directory with the same name as `VELOCITY_SYSTEM`. All -of the files in this directory will be copied to the BSD. +Next Velocity will copy any files that you specified. Parse Template ############## @@ -32,18 +30,20 @@ Finally velocity will run the `build` script and log the output to a file called Build Directory Layout ###################### -At the end of a build `VELOCITY_BUILD_DIR` should look somthing like this. With the addition of any other files that +At the end of a build the build dir should look somthing like this. With the addition of any other files that got copied in the :ref:`files` step. .. code-block:: bash . - ├── jknqsnkc - │ ├── build - │ ├── log - │ └── script - └── nfhmhmsh + ├── fedora-41-8a9a360 + │ ├── 8a9a360.sif + │ ├── build + │ ├── log + │ └── script + └── hello-world-1.0-de9c02b ├── build + ├── de9c02b.sif + ├── hello_world.py ├── log └── script - diff --git a/docs/build/html/_sources/reference/config.rst.txt b/docs/build/html/_sources/reference/config.rst.txt new file mode 100644 index 0000000..50a83c3 --- /dev/null +++ b/docs/build/html/_sources/reference/config.rst.txt @@ -0,0 +1,64 @@ +************* +Configuration +************* + +Configuration for Velocity comes from three places. Command line options, environment variables and the configuration file. + +Commandline Options +################### +These take the highest level of precedence and can be viewed by using the ``--help`` option in the command line. + +Variables +######### +Variables are the second highest level of configuration. + +`VELOCITY_IMAGE_PATH` +--------------------- + +This variable points to the directories containing the the image definitions. + +`VELOCITY_SYSTEM` +----------------- +This variable specifies what computer system you are building for (e.g. frontier). + +`VELOCITY_BACKEND` +------------------ +This variable specifies the container backend that should be used (e.g podman). + +`VELOCITY_DISTRO` +----------------- +This variable specifies the distro of the container images that will be built. This name is flexable and completely +up to the user. It is used purely for organizational purposes. + +`VELOCITY_BUILD_DIR` +-------------------- +This variable specifies a scratch space for Velocity to preform builds in. + +`VELOCITY_CONFIG_DIR` +--------------------- +This variable specifies where to look for the configuration file. + +Configuration File +################## +The configuration file is the lowest level of configuration. By default Velocity looks for ``config.yaml`` in +``~/.velocity`` unless ``VELOCITY_CONFIG_DIR`` is set. A number of configuration option for velocity can be set. + +.. code-block:: yaml + + velocity: + system: frontier + backend: apptainer + distro: ubuntu + debug: INFO # set the debug level + image_path: # a list of : seperated paths + build_dir: # path to a scratch space + +Additionally you can set :ref:`arguments` and :ref:`specVariables` at a global level in the constraints section. As an example here +we are adding ``--disable-cache`` as an argument for every image build we do with apptainer. + +.. code-block:: yaml + + constraints: + arguments: + - value: --disable-cache + when: backend=apptainer diff --git a/docs/build/html/_sources/reference/index.rst.txt b/docs/build/html/_sources/reference/index.rst.txt index f386b53..0c01598 100644 --- a/docs/build/html/_sources/reference/index.rst.txt +++ b/docs/build/html/_sources/reference/index.rst.txt @@ -8,6 +8,7 @@ Technical Docs .. toctree:: :maxdepth: 2 + config vtmp - specifications + specs build diff --git a/docs/build/html/_sources/reference/specifications.rst.txt b/docs/build/html/_sources/reference/specifications.rst.txt deleted file mode 100644 index b02e926..0000000 --- a/docs/build/html/_sources/reference/specifications.rst.txt +++ /dev/null @@ -1,160 +0,0 @@ -******************* -Specifications.yaml -******************* - -Basic Layout -############ -The `specifications.yaml` file defines image availability, dependencies and a number of other available -options for an image. Velocity doesn't care about any extraneous data you may have in it. All that Velocity looks -for is a base level section called `build_specifications`. Any other sections that you may add for your own purposes -are ignored by Velocity. Under `build_specifications` there should be a tree defining under what conditions the image -can be built in the order `system` > `backend` > `distro`. For example if I am creating an `centos` based image for use on -frontier which only has Apptainer on it then I should have the following structure in my `specifications.yaml` file. - -.. code-block:: yaml - - build_specifications: - frontier: - apptainer: - ubuntu: - # config options - -If I wanted it to be available on summit as well which has Apptainer and Podman I would add the following. - -.. code-block:: yaml - - build_specifications: - frontier: - apptainer: - centos: - # config options - summit: - apptainer: - centos: - # config options - podman: - centos: - # config options - -While `system` and `backend` are fairly self explanatory the `distro` is a little more obscure. Velocity does not do -any checks to make sure that the image being built is actually of the right distro. All that the distro tells Velocity to do is -look in the `templates` folder of an image for a template called `.vtmp` (e.g. centos.vtmp). If a template with -the right name cannot be found Velocity will error. - -.. code-block:: bash - :caption: image structure - - . - └── - └── - ├── specifications.yaml - └── templates - └── .vtmp - - -Config Options -############## -Under each `system` > `backend` > `distro` section there are a variaty of config options available. - -dependencies ------------- -The `dependencies` section is a list of images that must be built before the current one can be. - -.. code-block:: yaml - - build_specifications: - frontier: - apptainer: - ubuntu: - dependencies: - - dep_one - - dep_two - # ... - -Some times you may want a specific version of a dependency in which case Velocity provides several ways to specify dependency -relationships. Versions are compared by splitting the version string up based on periods and then comparing the sections -left to right alphanumerically. - -.. code-block:: yaml - - build_specifications: - frontier: - apptainer: - ubuntu: - dependencies: - - dep_one@=11.5 # equal to 11.5 - - dep_two@_11.5 # less than or equal to 11.5 - - dep_three@^11.5 # greater than or equal to 11.5 - - dep_four@11.2%11.8 # inbetween 11.2 and 11.8 (inclusive) - # ... - -prolog ------- -There are certain occasions when some commands need to be run on the host machine before an image is built. These -commands can be placed in a bash script in a directory which matches the system name. All that you need to do in the -`specifications.yaml` file is put the script name. - -.. code-block:: yaml - - build_specifications: - frontier: - apptainer: - ubuntu: - dependencies: - - dep_one - - dep_two - # ... - prolog: example_prolog.sh - -.. code-block:: bash - :caption: VELOCITY_IMAGE_DIR - - . - ├── - └── - ├── - │ └── example_prolog.sh - ├── specifications.yaml - └── templates - └── .vtmp - -arguments ---------- -Any arguments specified here will be added to the build command. - -.. code-block:: yaml - - build_specifications: - frontier: - apptainer: - ubuntu: - dependencies: - - dep_one - - dep_two - # ... - prolog: example_prolog.sh - arguments: - - '--build-arg EXAMPLE=8046' - -This will result in `apptainer build --build-arg EXAMPLE=8046 ...` when the image is built. - -variables ---------- -Here you can define :ref:`variables` for the VTMP file to use when it is parsed. - -.. code-block:: yaml - - build_specifications: - frontier: - apptainer: - ubuntu: - dependencies: - - dep_one - - dep_two - # ... - prolog: example_prolog.sh - arguments: - - '--build-arg EXAMPLE=8046' - variables: - - arch: x86_64 - - url: 'https://example.com' diff --git a/docs/build/html/_sources/reference/specs.rst.txt b/docs/build/html/_sources/reference/specs.rst.txt new file mode 100644 index 0000000..d3667f4 --- /dev/null +++ b/docs/build/html/_sources/reference/specs.rst.txt @@ -0,0 +1,114 @@ +******************* +Specs.yaml +******************* + +About +##### +The `specs.yaml` file defines image availability, dependencies and a number of other available +options for an image. + +Config Options +############## + +versions +-------- +The `versions` section defines a list of versions and their availability. + +.. code-block:: yaml + + versions: + - spec: 45 + when: distro=ubuntu + - spec: + - 46 + - 47 + when: backend=podman + +dependencies +------------ +The `dependencies` section is used to specify prerequist images. + +.. code-block:: yaml + + dependencies: + - spec: ubuntu + when: distro=ubuntu + - spec: + - "gcc@12:" + - pytorch + when: myapp@:2.3 + +templates +--------- +By default velocity looks for a template called ``default.vtmp``, but using the `templates` section you can specify an +alternate template. + +.. code-block:: yaml + + templates: + - name: second_tmp + when: "myapp@2:" + +.. _arguments: + +arguments +--------- +Use the `argument` section to add build time arguments to an image. + +.. code-block:: yaml + + arguments: + - value: --disable-cache + when: backend=apptainer + +.. _specVariables: + +variables +--------- +Use the `variables` section to set template variables. + +.. code-block:: yaml + + variables: + - name: test_var + value: "Test value." + +files +----- +Use the `files` section to specify files or directories in the `files` folder that you want copied to the build directory. + +.. code-block:: yaml + + files: + - name: config.json + +prologs +------- +Use the `prologs` section to bash commands that you want to run before the build. + +.. code-block:: yaml + + prologs: + - script: | + git clone ... + when: system=summit + +Using ``when`` +############## +A few notes about using ``when`` to filter config options. The ``when`` option can be used to filter configs as shown +above by system, backend, distro and dependencies. The only exception to this is the `versions` section which cannot +be filtered by dependencies. List each item that you want to filter by separated by a space e.g. ``gcc@12.3 system=frontier``. +Additionally you can specify the scope of a ``when`` by specifying the ``scope``. The default scope is ``image`` which means +that the when statement is evaluated on the current image. So say you want to apply a config in the gcc `specs.yaml` file +to every gcc version greater than 10.3.0, you would use ``when: ^ubuntu:``. Alternatively you can set the scope to ``build`` +when you want the when statement evaluated on the current build. + +.. code-block:: yaml + + variable: + - name: TEST + value: "1 2 3 4" + when: ubuntu + scope: build + +This can be read as "If the ubuntu image is in the current set of images to be built then add the TEST variable." diff --git a/docs/build/html/_sources/reference/vtmp.rst.txt b/docs/build/html/_sources/reference/vtmp.rst.txt index fe621ed..2c05c5a 100644 --- a/docs/build/html/_sources/reference/vtmp.rst.txt +++ b/docs/build/html/_sources/reference/vtmp.rst.txt @@ -3,76 +3,94 @@ VTMP Format *********** VTMP files are used by Velocity to generate the appropriate script for a particular backend. -While certain features are supported across all backends, others are exclusive to one backend and many are interpreted -quite differently between the different backends. The goal of the VTMP files are to provide a unified API for +The goal of the VTMP files are to provide a unified API for container images across multiple backends but they are not feature exhaustive. VTMP files are split up into a number of sections. .. code-block:: text :caption: Example VTMP + >>> this is a comment @from - # can only take one of these + >>> can only take one of these examples docker.io/ubuntu:20.04 - /example.sif # only works for apptainer backend - %(__base__) + /example.sif >>> only works for apptainer backend + {{ __base__ }} >>> build on the previous image - @pre # same as post but placed right after @from - # anything here is taken literally - # denote the start of a line with | - |echo 'Without tab' + @pre + >>> anything here is taken literally + >>> optionally denote the start of a line with | (helpful for white space) + echo 'Without tab' | echo 'With tab' - @arg - # define arg names - example_arg # reference this argument throughout the script with @() - @copy - # list files to copy, format + >>> list files to copy, format /external/file /internal/file @run - # list of commands to run in container + >>> list of commands to run in container echo 'Hello world' - ?podman echo 'podman' - ?apptainer echo 'apptainer' - echo %(template_variable) - echo @(example_arg) # see @arg + ?? distro=podman |> echo 'podman' ?? >>> conditional + ?? distro=apptainer |> echo 'apptainer' ?? + echo {{ template_variable }} + echo @@ example_arg @@ >>> define an argument + !envar TEST_VAR I want this here and in the @env section >>> the !envar directive @env - # set env variables + >>> set env variables PATH /something:$PATH @label - # set container labels - velocity.image.%(__name__)__%(__tag__) %(__hash__) - + >>> set container labels + IMAGE_NAME {{ __name__ }} @entry - # set entrypoint or %runscript - # only one line allowed + >>> set ENTRYPOINT or %runscript + >>> only one line allowed + /bin/bash - @post # same as pre but placed after everything else + @post >>> same as pre but placed after everything else .. _variables: Variables ######### The first thing that is done to a VTMP file is the substitution of any variables found in the script. Variables are indicated -by `%()`. In Velocity variables can be defined by the user in the `specifications.yaml` file, but Velocity also +by ``{{ }}``. In Velocity variables can be defined by the user in the `specs.yaml` file, but Velocity also provides a set of build time variables. .. code-block:: text __backend__ __distro__ - __hash__ # template hash - __base__ # previous image to build on - __name__ # current image name e.g. cuda + __base__ >>> previous image to build on + __name__ >>> current image name e.g. cuda __system__ - __tag__ # image "version" e.g. 11.7.1 + __version__ >>> image "version" e.g. 11.7.1 + __version_major__ + __version_minor__ + __version_patch__ + __version_suffix__ __timestamp__ + >>> the versions of all images in a build are also available in the following format + ____version__ + ____version_major__ + ____version_minor__ + ____version_patch__ + ____version_suffix__ + +Arguments +######### +You can also pass in arguments at build time using the form ``@@ @@``, but don't forget to add the +argument in ``specs.yaml`` so that is gets added to the build command or the build will fail. + +Conditionals +############ +If there is a line in a template that you only want under certain conditions use ``?? |> ??``. +The whole conditional will be replaced with the section if the condition is true; otherwise, it will substitute +an empty string. The conditionals can include the following components, ``system=``, ``backend=``, +``distro=`` and a dependency in the form ``^``. Sections ######## @@ -92,12 +110,6 @@ allows you to add white space to the beginning of a line. The only difference be are where they are placed in the script the `@pre` section is placed at the beginning of the script right after the `@from` section. -@arg ----- -The `@arg` section specifies build time arguments that are passed to the backend. If these arguments are not passed to -the backend at build time it will fail. Once an arg has been defined in the `@arg` section it can be referenced -throughout the script with `@()`. - @copy ----- The `@copy` section takes a list of files/dir to be copied in the format. @@ -105,12 +117,14 @@ The `@copy` section takes a list of files/dir to be copied in the f @run ---- The `@run` section takes a list of commands to be run in the container. These commands should be written as if they all -occur one after the other in the same shell. +occur one after the other in the same shell. One added feature to the @run section is the ``!envar`` directive. This +directive will add the following variable definition to the @env section as well as to the current @run section. Use +the format ``!envar ``. @env ---- The `@env` section sets environment variables. These variables will only be available when the container is run or in -the next build so if there is a variable that is needed in the run section you must declare it there as well. +the next build so if there is a variable that is needed in the run section use the ``!envar`` directive in the @run section. @label ------ @@ -125,8 +139,3 @@ in the `@entry` section. @post ----- See :ref:`@pre `. The `@post` section is placed at the end of the script after all other sections. - -Conditionals -############ -It is handy somtimes to be able to limit certain commands to a backend(s) this can be done by placing a `?` -at the beginning of the line in question. diff --git a/docs/build/html/_sources/starting/basic.rst.txt b/docs/build/html/_sources/starting/basic.rst.txt index 8c4abb5..99d4beb 100644 --- a/docs/build/html/_sources/starting/basic.rst.txt +++ b/docs/build/html/_sources/starting/basic.rst.txt @@ -3,131 +3,82 @@ Overview ******** -Concept -####### -Velocity is a tool to help with the maintenance of a variety of container builds on multiple systems, backends -(e.g podman or apptainer) and distros. - How it Works ############ Velocity works by building a set of containers in a chain so that the final container has all of the needed components. - - -Layout -###### -The contents of the Velocity repo are fairly simple. - -.. code-block:: - - . - ├── docs - ├── lib - ├── README.md - ├── setup-env.sh - └── velocity - -For informational purposes there is a folder `docs` which holds this documentation and a README file. -The `setup-env.sh` script can be used to 'install' velocity. The lib folder holds a python package which the -velocity script needs to run. - - - +Velocity maintains a very hands off approach. It is only as good as the templates/configuration that you write. +In general it will assume that a particular build will work unless you tell it otherwise. It is important to note +that while Velocity has many features that are similar to those provided by a package manager, it is NOT a +package manager. Rather it should be viewed as a templating and build orchestration tool. Installation ############ -First you will need to set up a conda environment for Velocity and install the following packages: - -.. note:: - - For more info on creating a custom conda environment on OLCF systems visit https://docs.olcf.ornl.gov/software/python/index.html#custom-environments. - -.. code-block:: - - conda install pyyaml networkx colorama python-editor - -.. important:: - - If you wish to build the docs you will also need to install `sphinx` and `sphinx-rtd-theme`. - -Next clone the Velocity git repository to the desired location. +The easiest way to install velocity is to install prebuilt python packages using pip. .. code-block:: bash - git clone https://gitlab.ccs.ornl.gov/saue-software/velocity.git + pip install olcf-velocity -You can then setup Velocity by sourcing the `setup-env.sh` script. +You can also clone the velocity repository and build/install velocity from source. .. code-block:: bash - . ./velocity/setup-env.sh - -The `setup-env.sh` script will help you choose the value of several environment :ref:`variables` -that velocity uses. + git clone https://github.com/olcf/velocity.git + cd velocity + python3 -m build + # install the built python wheel package + pip install dist/olcf-velocity-*.whl -You should now be able to run the `velocity` command. +Now you can use Velocity as a python module! We recommend setting a bash alias for convenience. .. code-block:: bash + user@hostname:~$ alias velocity="python3 -m velocity" user@hostname:~$ velocity - ==> System: x86_64 - ==> Backend: podman - ==> Distro: fedora + usage: velocity [-h] [-v] [-D {TRACE,DEBUG,INFO,SUCCESS,WARNING,ERROR,CRITICAL}] [-b BACKEND] [-s SYSTEM] [-d DISTRO] {build,avail,spec} ... - usage: velocity [-h] [-v] [-b BACKEND] [-s SYSTEM] [-d DISTRO] - {build,avail,spec,edit} ... - - Build tool for OLCF containers + build tool for OLCF containers positional arguments: - {build,avail,spec,edit} + {build,avail,spec} build build specified container image avail lookup available images spec lookup image dependencies - edit edit image files options: -h, --help show this help message and exit -v, --version program version + -D {TRACE,DEBUG,INFO,SUCCESS,WARNING,ERROR,CRITICAL}, --debug {TRACE,DEBUG,INFO,SUCCESS,WARNING,ERROR,CRITICAL} + set debug output level -b BACKEND, --backend BACKEND -s SYSTEM, --system SYSTEM -d DISTRO, --distro DISTRO - See (https://gitlab.ccs.ornl.gov/saue-software/velocity) - + See https://github.com/olcf/velocity .. _configuration: Configuration ############# -There are five system variables that need to be set for Velocity to work (these are set in the `setup-env.sh` script). +Velocity has a number of configuration options. The basic ones are setting the system name, container backend, +container distro, the path(s) to your image definitions and the build scratch directory. +To see more configuration options go to the :doc:`configuration ` page. The easiest way to configure velocity is to +edit ``~/.velocity/config.yaml``. -`VELOCITY_IMAGE_DIR` --------------------- -This variable points to the directory containing the the image definitions. +.. code-block:: + + velocity: + system: frontier + backend: apptainer + distro: ubuntu + image_path: # a list of : seperated paths + build_dir: # path to a scratch space .. note:: Image definitions can be created by the user as needed but a base set for usage at OLCF are provided at - `https://gitlab.ccs.ornl.gov/saue-software/velocity-images.git` - -`VELOCITY_SYSTEM` ------------------ -This variable specifies what computer system you are building for (e.g. frontier). - -`VELOCITY_BACKEND` ------------------- -This variable specifies the container backend that should be used (e.g podman). - -`VELOCITY_DISTRO` ------------------ -This variable specifies the distro of the container images that will be built. - -`VELOCITY_BUILD_DIR` --------------------- -This variable specifies a scratch space for Velocity to preform builds in. - - + https://github.com/olcf/velocity-images Basic Usage ########### @@ -140,27 +91,20 @@ The `avail` command prints the defined images that can be built. .. code-block:: bash user@hostname:~$ velocity avail - ==> System: x86_64 - ==> Backend: podman - ==> Distro: centos - - ==> centos - stream8 ==> gcc - 11.2.0 - ==> hmmer - 3.4 - ==> kalign - 3.4.0 - ==> miniforge3 - 23.11.0 - ==> python - 3.11.8 - ==> pytorch - latest - -Each image is listed and then indented underneath is a list of the available versions -(in velocity they are called tags). + 12.3.0 + 13.2.0 + 14.1.0 + ==> mpich + 3.4.3 + ==> rocm + 5.7.1 + ==> ubuntu + 20.04 + 22.04 + 24.04 + +Each image is listed and then indented underneath is a list of the available versions. `spec` ------ @@ -170,37 +114,28 @@ The `spec` command shows the dependencies for a given image (or list of images) .. code-block:: bash user@hostname:~$ velocity spec pytorch - ==> System: summit - ==> Backend: podman - ==> Distro: centos - - > pytorch@=latest - ^cuda@=11.7.1 - ^centos@=stream8 - ^cudnn@=8.5.0.96 - ^cuda@=11.7.1 - ^centos@=stream8 - ^spectrum-mpi@=10.4.0.6 - ^centos@=stream8 - ^gcc@=11.2.0 - ^centos@=stream8 - ^miniforge3@=23.11.0 - ^centos@=stream8 - + > pytorch@latest + ^cuda@11.7.1 + ^centos@stream8 + ^cudnn@8.5.0.96 + ^cuda@11.7.1 + ^centos@stream8 + ^spectrum-mpi@10.4.0.6 + ^centos@stream8 + ^gcc@11.2.0 + ^centos@stream8 + ^miniforge3@23.11.0 + ^centos@stream8 `build` ------- -The `build` can be used to build an container image from one or more image definitions. +The `build` command can be used to build an container image from one or more image definitions. .. code-block:: bash user@hostname:~$ velocity build centos - ==> System: x86_64 - ==> Backend: podman - ==> Distro: centos - ==> Build Order: centos@=stream8 @@ -214,10 +149,6 @@ Both the spec and the build command can also take a list of images. .. code-block:: bash user@hostname:~$ velocity build gcc python - ==> System: x86_64 - ==> Backend: podman - ==> Distro: centos - ==> Build Order: centos@=stream8 gcc@=11.2.0 @@ -237,20 +168,3 @@ Both the spec and the build command can also take a list of images. ==> sunflyhd: GENERATING SCRIPT ... ==> sunflyhd: BUILDING ... ==> sunflyhd: IMAGE localhost/python__3.11.8__x86_64__centos:latest (python@=3.11.8) BUILT [0:23:19] - -`edit` ------- -The edit command can be used to edit the VTMP or specification file for an image. By default -it edits the VTMP file. Add `-s` to edit the `specifications.yaml` file. - -.. code-block:: bash - - user@hostname:~$ velocity edit --help - usage: velocity edit [-h] [-s] target - - positional arguments: - target image to edit - - options: - -h, --help show this help message and exit - -s, --specification edit the specifications file diff --git a/docs/build/html/_sources/starting/tutorial.rst.txt b/docs/build/html/_sources/starting/tutorial.rst.txt index 2214604..d232464 100644 --- a/docs/build/html/_sources/starting/tutorial.rst.txt +++ b/docs/build/html/_sources/starting/tutorial.rst.txt @@ -2,395 +2,261 @@ Creating Your First Image ************************* -Prerequisites -############# -You will need to have the Velocity repo cloned and the following environment variables set. You can do this by -sourcing the `setup-env.sh` script. - -.. code-block:: bash - - VELOCITY_IMAGE_DIR= - VELOCITY_BACKEND=podman # if you use apptainer you will need to make changes to some of the examples - VELOCITY_BUILD_DIR=/tmp/velocity/build - VELOCITY_SYSTEM=x86_64 # if you use a different system name you will need to make changes to some of the examples - VELOCITY_ROOT= # you will also need to add this to PATH - VELOCITY_DISTRO=fedora - Base Image ########## -Let's start with a simple base image. This image will pull an fedora docker image and update the packages. -Start by creating a directory in the image directory called `fedora` (for this tutorial we are starting with an empty -image directory). Next we need to create a directory in the `fedora` directory for the version of fedora that we want. -Let's use `38`. In this directory create a file called `specifications.yaml` and a directory called `templates` with -a file named `fedora.vtmp`. Your image directory and files should now look like this. +Let's start with a simple base image. This image will pull an fedora docker image and update the packages. For this +tutorial I have created the empty directory ``/tmp/velocity/images`` and configured it as the image directory. I have set +the build directory to ``/tmp/velocity/build``, the backend to ``apptainer`` and the distro as ``fedora``. All +commands are run in ``/tmp/velocity``. +Start by creating a directory in the image directory called ``fedora``. In this directory create a file called +``specs.yaml`` and a directory called ``templates`` with +a file named ``default.vtmp``. Your image directory and files should now look like this. .. code-block:: bash - :caption: VELOCITY_IMAGE_DIR + :caption: /tmp/velocity/images + + fedora + ├── specs.yaml + └── templates + └── default.vtmp - . - └── fedora - └── 38 - ├── specifications.yaml - └── templates - └── fedora.vtmp .. code-block:: yaml - :caption: fedora/38/specifications.yaml + :caption: specs.yaml - build_specifications: + versions: + - spec: 38 + when: distro=fedora - x86_64: - podman: - fedora: {} .. code-block:: text - :caption: fedora/38/templates/fedora.vtmp + :caption: default.vtmp @from - docker.io/fedora:38 + docker.io/fedora:{{ __version__ }} @run dnf -y upgrade - - @label - velocity.config.system %(__system__) - velocity.config.backend %(__backend__) - velocity.config.distro %(__distro__) - velocity.image.%(__name__)__%(__tag__) %(__hash__) + dnf clean all Now if you run `velocity avail` you should get the following. .. code-block:: bash user@hostname:~$ velocity avail - ==> System: x86_64 - ==> Backend: podman - ==> Distro: fedora - ==> fedora - 38 + 38 Now build the image. .. code-block:: bash user@hostname:~$ velocity build fedora - ==> System: x86_64 - ==> Backend: podman - ==> Distro: fedora - ==> Build Order: - fedora@=38 + fedora@38-aa51aa7 + + ==> aa51aa7: BUILD fedora@38 ... + ==> aa51aa7: GENERATING SCRIPT ... + ==> aa51aa7: BUILDING ... + ==> aa51aa7: IMAGE /tmp/velocity/build/fedora-38-aa51aa7/aa51aa7.sif (fedora@38) BUILT [0:01:07] - ==> yftozouc: BUILD fedora@=38 ... - ==> yftozouc: GENERATING SCRIPT ... - ==> yftozouc: BUILDING ... - ==> yftozouc: IMAGE localhost/fedora__38__x86_64__fedora:latest (fedora@=38) BUILT [0:07:10] + ==> BUILT: /tmp/velocity/fedora-38__x86_64-fedora.sif If you wish to see more output you can add the `-v` flag: .. code-block:: bash user@hostname:~$ velocity build fedora -v - ==> System: x86_64 - ==> Backend: podman - ==> Distro: fedora - ==> Build Order: - fedora@=38 - - ==> xuoykrdt: BUILD fedora@=38 ... - ==> xuoykrdt: GENERATING SCRIPT ... - SCRIPT: /tmp/velocity/build/xuoykrdt/script - FROM docker.io/fedora:38 - - RUN dnf -y upgrade - - LABEL velocity.config.system="x86_64" \ - velocity.config.backend="podman" \ - velocity.config.distro="fedora" \ - velocity.image.fedora__38="ff9fa85cf102560cf3fe2014c3c758fbb3809247537abbeab2c4b67c62dda164" - - ==> xuoykrdt: BUILDING ... - #!/usr/bin/env bash - podman build -f /tmp/velocity/build/xuoykrdt/script -t localhost/fedora__38__x86_64__fedora:latest .; - STEP 1/3: FROM docker.io/fedora:38 - STEP 2/3: RUN dnf -y upgrade - Fedora 38 - x86_64 131 kB/s | 84 MB 10:53 - Fedora 38 openh264 (From Cisco) - x86_64 2.9 kB/s | 2.6 kB 00:00 - Fedora Modular 38 - x86_64 1.1 MB/s | 2.8 MB 00:02 - Fedora 38 - x86_64 - Updates 4.5 MB/s | 40 MB 00:09 - Fedora Modular 38 - x86_64 - Updates 152 kB/s | 2.1 MB 00:14 - Last metadata expiration check: 0:00:01 ago on Wed Mar 27 20:09:54 2024. - Dependencies resolved. - ================================================================================ - Package Arch Version Repo Size - ================================================================================ - Upgrading: - curl x86_64 8.0.1-7.fc38 updates 348 k - dnf noarch 4.19.0-1.fc38 updates 507 k - dnf-data noarch 4.19.0-1.fc38 updates 39 k - elfutils-default-yama-scope noarch 0.191-1.fc38 updates 12 k - elfutils-libelf x86_64 0.191-1.fc38 updates 208 k - elfutils-libs x86_64 0.191-1.fc38 updates 263 k - expat x86_64 2.6.0-1.fc38 updates 112 k - keyutils-libs x86_64 1.6.3-1.fc38 updates 31 k - libcurl x86_64 8.0.1-7.fc38 updates 315 k - libdnf x86_64 0.73.0-1.fc38 updates 681 k - libgcc x86_64 13.2.1-7.fc38 updates 115 k - libgomp x86_64 13.2.1-7.fc38 updates 324 k - libsolv x86_64 0.7.28-1.fc38 updates 426 k - libstdc++ x86_64 13.2.1-7.fc38 updates 870 k - ncurses-base noarch 6.4-7.20230520.fc38.1 updates 88 k - ncurses-libs x86_64 6.4-7.20230520.fc38.1 updates 336 k - python3 x86_64 3.11.8-2.fc38 updates 28 k - python3-dnf noarch 4.19.0-1.fc38 updates 606 k - python3-hawkey x86_64 0.73.0-1.fc38 updates 107 k - python3-libdnf x86_64 0.73.0-1.fc38 updates 859 k - python3-libs x86_64 3.11.8-2.fc38 updates 9.6 M - systemd-libs x86_64 253.17-1.fc38 updates 649 k - vim-data noarch 2:9.1.158-1.fc38 updates 23 k - vim-minimal x86_64 2:9.1.158-1.fc38 updates 808 k - yum noarch 4.19.0-1.fc38 updates 37 k - - Transaction Summary - ================================================================================ - Upgrade 25 Packages - - Total download size: 17 M - Downloading Packages: - (1/25): dnf-data-4.19.0-1.fc38.noarch.rpm 107 kB/s | 39 kB 00:00 - (2/25): elfutils-default-yama-scope-0.191-1.fc3 224 kB/s | 12 kB 00:00 - (3/25): curl-8.0.1-7.fc38.x86_64.rpm 637 kB/s | 348 kB 00:00 - (4/25): dnf-4.19.0-1.fc38.noarch.rpm 807 kB/s | 507 kB 00:00 - (5/25): elfutils-libelf-0.191-1.fc38.x86_64.rpm 901 kB/s | 208 kB 00:00 - (6/25): expat-2.6.0-1.fc38.x86_64.rpm 1.1 MB/s | 112 kB 00:00 - (7/25): keyutils-libs-1.6.3-1.fc38.x86_64.rpm 331 kB/s | 31 kB 00:00 - (8/25): elfutils-libs-0.191-1.fc38.x86_64.rpm 1.1 MB/s | 263 kB 00:00 - (9/25): libgcc-13.2.1-7.fc38.x86_64.rpm 872 kB/s | 115 kB 00:00 - (10/25): libcurl-8.0.1-7.fc38.x86_64.rpm 1.7 MB/s | 315 kB 00:00 - (11/25): libgomp-13.2.1-7.fc38.x86_64.rpm 1.8 MB/s | 324 kB 00:00 - (12/25): libdnf-0.73.0-1.fc38.x86_64.rpm 1.8 MB/s | 681 kB 00:00 - (13/25): libsolv-0.7.28-1.fc38.x86_64.rpm 2.1 MB/s | 426 kB 00:00 - (14/25): ncurses-base-6.4-7.20230520.fc38.1.noa 423 kB/s | 88 kB 00:00 - (15/25): ncurses-libs-6.4-7.20230520.fc38.1.x86 1.3 MB/s | 336 kB 00:00 - (16/25): python3-3.11.8-2.fc38.x86_64.rpm 286 kB/s | 28 kB 00:00 - (17/25): libstdc++-13.2.1-7.fc38.x86_64.rpm 1.8 MB/s | 870 kB 00:00 - (18/25): python3-hawkey-0.73.0-1.fc38.x86_64.rp 834 kB/s | 107 kB 00:00 - (19/25): python3-dnf-4.19.0-1.fc38.noarch.rpm 2.5 MB/s | 606 kB 00:00 - (20/25): python3-libdnf-0.73.0-1.fc38.x86_64.rp 1.6 MB/s | 859 kB 00:00 - (21/25): systemd-libs-253.17-1.fc38.x86_64.rpm 1.4 MB/s | 649 kB 00:00 - (22/25): vim-data-9.1.158-1.fc38.noarch.rpm 196 kB/s | 23 kB 00:00 - (23/25): yum-4.19.0-1.fc38.noarch.rpm 270 kB/s | 37 kB 00:00 - (24/25): vim-minimal-9.1.158-1.fc38.x86_64.rpm 2.5 MB/s | 808 kB 00:00 - (25/25): python3-libs-3.11.8-2.fc38.x86_64.rpm 3.6 MB/s | 9.6 MB 00:02 - -------------------------------------------------------------------------------- - Total 3.6 MB/s | 17 MB 00:04 - Running transaction check - Transaction check succeeded. - Running transaction test - Transaction test succeeded. - Running transaction - Preparing : 1/1 - Upgrading : libgcc-13.2.1-7.fc38.x86_64 1/50 - Running scriptlet: libgcc-13.2.1-7.fc38.x86_64 1/50 - Upgrading : libstdc++-13.2.1-7.fc38.x86_64 2/50 - Upgrading : libsolv-0.7.28-1.fc38.x86_64 3/50 - Upgrading : libdnf-0.73.0-1.fc38.x86_64 4/50 - Upgrading : vim-data-2:9.1.158-1.fc38.noarch 5/50 - Upgrading : ncurses-base-6.4-7.20230520.fc38.1.noarch 6/50 - Upgrading : ncurses-libs-6.4-7.20230520.fc38.1.x86_64 7/50 - Upgrading : libcurl-8.0.1-7.fc38.x86_64 8/50 - Upgrading : expat-2.6.0-1.fc38.x86_64 9/50 - Upgrading : python3-3.11.8-2.fc38.x86_64 10/50 - Upgrading : python3-libs-3.11.8-2.fc38.x86_64 11/50 - Upgrading : python3-libdnf-0.73.0-1.fc38.x86_64 12/50 - Upgrading : python3-hawkey-0.73.0-1.fc38.x86_64 13/50 - Upgrading : elfutils-libelf-0.191-1.fc38.x86_64 14/50 - Upgrading : elfutils-default-yama-scope-0.191-1.fc38.noarch 15/50 - Running scriptlet: elfutils-default-yama-scope-0.191-1.fc38.noarch 15/50 - Upgrading : dnf-data-4.19.0-1.fc38.noarch 16/50 - Upgrading : python3-dnf-4.19.0-1.fc38.noarch 17/50 - Upgrading : dnf-4.19.0-1.fc38.noarch 18/50 - Running scriptlet: dnf-4.19.0-1.fc38.noarch 18/50 - Upgrading : yum-4.19.0-1.fc38.noarch 19/50 - Upgrading : elfutils-libs-0.191-1.fc38.x86_64 20/50 - Upgrading : curl-8.0.1-7.fc38.x86_64 21/50 - Upgrading : vim-minimal-2:9.1.158-1.fc38.x86_64 22/50 - Upgrading : systemd-libs-253.17-1.fc38.x86_64 23/50 - Upgrading : libgomp-13.2.1-7.fc38.x86_64 24/50 - Upgrading : keyutils-libs-1.6.3-1.fc38.x86_64 25/50 - Cleanup : elfutils-libs-0.190-2.fc38.x86_64 26/50 - Cleanup : systemd-libs-253.15-2.fc38.x86_64 27/50 - Cleanup : vim-minimal-2:9.1.113-1.fc38.x86_64 28/50 - Cleanup : curl-8.0.1-6.fc38.x86_64 29/50 - Cleanup : yum-4.18.2-1.fc38.noarch 30/50 - Running scriptlet: dnf-4.18.2-1.fc38.noarch 31/50 - Cleanup : dnf-4.18.2-1.fc38.noarch 31/50 - Running scriptlet: dnf-4.18.2-1.fc38.noarch 31/50 - Cleanup : python3-dnf-4.18.2-1.fc38.noarch 32/50 - Cleanup : dnf-data-4.18.2-1.fc38.noarch 33/50 - Cleanup : vim-data-2:9.1.113-1.fc38.noarch 34/50 - Cleanup : elfutils-default-yama-scope-0.190-2.fc38.noarch 35/50 - Cleanup : python3-hawkey-0.72.0-1.fc38.x86_64 36/50 - Cleanup : python3-libdnf-0.72.0-1.fc38.x86_64 37/50 - Cleanup : libdnf-0.72.0-1.fc38.x86_64 38/50 - Cleanup : libstdc++-13.2.1-4.fc38.x86_64 39/50 - Cleanup : python3-libs-3.11.7-2.fc38.x86_64 40/50 - Cleanup : python3-3.11.7-2.fc38.x86_64 41/50 - Cleanup : ncurses-libs-6.4-7.20230520.fc38.x86_64 42/50 - Cleanup : ncurses-base-6.4-7.20230520.fc38.noarch 43/50 - Cleanup : expat-2.5.0-2.fc38.x86_64 44/50 - Cleanup : libgcc-13.2.1-4.fc38.x86_64 45/50 - Running scriptlet: libgcc-13.2.1-4.fc38.x86_64 45/50 - Cleanup : libsolv-0.7.27-1.fc38.x86_64 46/50 - Cleanup : libcurl-8.0.1-6.fc38.x86_64 47/50 - Cleanup : elfutils-libelf-0.190-2.fc38.x86_64 48/50 - Cleanup : libgomp-13.2.1-4.fc38.x86_64 49/50 - Cleanup : keyutils-libs-1.6.1-6.fc38.x86_64 50/50 - Running scriptlet: keyutils-libs-1.6.1-6.fc38.x86_64 50/50 - Verifying : curl-8.0.1-7.fc38.x86_64 1/50 - Verifying : curl-8.0.1-6.fc38.x86_64 2/50 - Verifying : dnf-4.19.0-1.fc38.noarch 3/50 - Verifying : dnf-4.18.2-1.fc38.noarch 4/50 - Verifying : dnf-data-4.19.0-1.fc38.noarch 5/50 - Verifying : dnf-data-4.18.2-1.fc38.noarch 6/50 - Verifying : elfutils-default-yama-scope-0.191-1.fc38.noarch 7/50 - Verifying : elfutils-default-yama-scope-0.190-2.fc38.noarch 8/50 - Verifying : elfutils-libelf-0.191-1.fc38.x86_64 9/50 - Verifying : elfutils-libelf-0.190-2.fc38.x86_64 10/50 - Verifying : elfutils-libs-0.191-1.fc38.x86_64 11/50 - Verifying : elfutils-libs-0.190-2.fc38.x86_64 12/50 - Verifying : expat-2.6.0-1.fc38.x86_64 13/50 - Verifying : expat-2.5.0-2.fc38.x86_64 14/50 - Verifying : keyutils-libs-1.6.3-1.fc38.x86_64 15/50 - Verifying : keyutils-libs-1.6.1-6.fc38.x86_64 16/50 - Verifying : libcurl-8.0.1-7.fc38.x86_64 17/50 - Verifying : libcurl-8.0.1-6.fc38.x86_64 18/50 - Verifying : libdnf-0.73.0-1.fc38.x86_64 19/50 - Verifying : libdnf-0.72.0-1.fc38.x86_64 20/50 - Verifying : libgcc-13.2.1-7.fc38.x86_64 21/50 - Verifying : libgcc-13.2.1-4.fc38.x86_64 22/50 - Verifying : libgomp-13.2.1-7.fc38.x86_64 23/50 - Verifying : libgomp-13.2.1-4.fc38.x86_64 24/50 - Verifying : libsolv-0.7.28-1.fc38.x86_64 25/50 - Verifying : libsolv-0.7.27-1.fc38.x86_64 26/50 - Verifying : libstdc++-13.2.1-7.fc38.x86_64 27/50 - Verifying : libstdc++-13.2.1-4.fc38.x86_64 28/50 - Verifying : ncurses-base-6.4-7.20230520.fc38.1.noarch 29/50 - Verifying : ncurses-base-6.4-7.20230520.fc38.noarch 30/50 - Verifying : ncurses-libs-6.4-7.20230520.fc38.1.x86_64 31/50 - Verifying : ncurses-libs-6.4-7.20230520.fc38.x86_64 32/50 - Verifying : python3-3.11.8-2.fc38.x86_64 33/50 - Verifying : python3-3.11.7-2.fc38.x86_64 34/50 - Verifying : python3-dnf-4.19.0-1.fc38.noarch 35/50 - Verifying : python3-dnf-4.18.2-1.fc38.noarch 36/50 - Verifying : python3-hawkey-0.73.0-1.fc38.x86_64 37/50 - Verifying : python3-hawkey-0.72.0-1.fc38.x86_64 38/50 - Verifying : python3-libdnf-0.73.0-1.fc38.x86_64 39/50 - Verifying : python3-libdnf-0.72.0-1.fc38.x86_64 40/50 - Verifying : python3-libs-3.11.8-2.fc38.x86_64 41/50 - Verifying : python3-libs-3.11.7-2.fc38.x86_64 42/50 - Verifying : systemd-libs-253.17-1.fc38.x86_64 43/50 - Verifying : systemd-libs-253.15-2.fc38.x86_64 44/50 - Verifying : vim-data-2:9.1.158-1.fc38.noarch 45/50 - Verifying : vim-data-2:9.1.113-1.fc38.noarch 46/50 - Verifying : vim-minimal-2:9.1.158-1.fc38.x86_64 47/50 - Verifying : vim-minimal-2:9.1.113-1.fc38.x86_64 48/50 - Verifying : yum-4.19.0-1.fc38.noarch 49/50 - Verifying : yum-4.18.2-1.fc38.noarch 50/50 - - Upgraded: - curl-8.0.1-7.fc38.x86_64 - dnf-4.19.0-1.fc38.noarch - dnf-data-4.19.0-1.fc38.noarch - elfutils-default-yama-scope-0.191-1.fc38.noarch - elfutils-libelf-0.191-1.fc38.x86_64 - elfutils-libs-0.191-1.fc38.x86_64 - expat-2.6.0-1.fc38.x86_64 - keyutils-libs-1.6.3-1.fc38.x86_64 - libcurl-8.0.1-7.fc38.x86_64 - libdnf-0.73.0-1.fc38.x86_64 - libgcc-13.2.1-7.fc38.x86_64 - libgomp-13.2.1-7.fc38.x86_64 - libsolv-0.7.28-1.fc38.x86_64 - libstdc++-13.2.1-7.fc38.x86_64 - ncurses-base-6.4-7.20230520.fc38.1.noarch - ncurses-libs-6.4-7.20230520.fc38.1.x86_64 - python3-3.11.8-2.fc38.x86_64 - python3-dnf-4.19.0-1.fc38.noarch - python3-hawkey-0.73.0-1.fc38.x86_64 - python3-libdnf-0.73.0-1.fc38.x86_64 - python3-libs-3.11.8-2.fc38.x86_64 - systemd-libs-253.17-1.fc38.x86_64 - vim-data-2:9.1.158-1.fc38.noarch - vim-minimal-2:9.1.158-1.fc38.x86_64 - yum-4.19.0-1.fc38.noarch - - Complete! - --> 787eba03a2e - STEP 3/3: LABEL velocity.config.system="x86_64" velocity.config.backend="podman" velocity.config.distro="fedora" velocity.image.fedora__38="ff9fa85cf102560cf3fe2014c3c758fbb3809247537abbeab2c4b67c62dda164" - COMMIT localhost/fedora__38__x86_64__fedora:latest - --> 17c3457c281 - Successfully tagged localhost/fedora__38__x86_64__fedora:latest - 17c3457c281309909691b183b77323eb56390792a787e4a319b494d40868c907 - ==> xuoykrdt: IMAGE localhost/fedora__38__x86_64__fedora:latest (fedora@=38) BUILT [0:13:27] + fedora@38-aa51aa7 + + ==> aa51aa7: BUILD fedora@38 ... + ==> aa51aa7: GENERATING SCRIPT ... + SCRIPT: /tmp/velocity/build/fedora-38-aa51aa7/script + Bootstrap: docker + From: docker.io/fedora:38 + + %post + dnf -y upgrade + dnf clean all + ==> aa51aa7: BUILDING ... + #!/usr/bin/env bash + apptainer build --disable-cache /tmp/velocity/build/fedora-38-aa51aa7/aa51aa7.sif /tmp/velocity/build/fedora-38-aa51aa7/script; + Fedora 38 - x86_64 2.3 MB/s | 83 MB 00:35 + Fedora 38 openh264 (From Cisco) - x86_64 2.8 kB/s | 2.6 kB 00:00 + Fedora Modular 38 - x86_64 1.8 MB/s | 2.8 MB 00:01 + Fedora 38 - x86_64 - Updates 2.8 MB/s | 42 MB 00:14 + Fedora Modular 38 - x86_64 - Updates 257 kB/s | 2.2 MB 00:08 + Dependencies resolved. + ================================================================================ + Package Arch Version Repo Size + ================================================================================ + Upgrading: + fedora-release-common noarch 38-37 updates 20 k + fedora-release-container noarch 38-37 updates 10 k + fedora-release-identity-container noarch 38-37 updates 12 k + glibc x86_64 2.37-19.fc38 updates 2.1 M + glibc-common x86_64 2.37-19.fc38 updates 320 k + glibc-minimal-langpack x86_64 2.37-19.fc38 updates 42 k + gnutls x86_64 3.8.5-1.fc38 updates 1.1 M + libnghttp2 x86_64 1.52.0-3.fc38 updates 75 k + python-pip-wheel noarch 22.3.1-4.fc38 updates 1.4 M + python3 x86_64 3.11.9-2.fc38 updates 28 k + python3-libs x86_64 3.11.9-2.fc38 updates 9.6 M + tpm2-tss x86_64 4.0.2-1.fc38 updates 391 k + vim-data noarch 2:9.1.393-1.fc38 updates 23 k + vim-minimal x86_64 2:9.1.393-1.fc38 updates 810 k + Installing weak dependencies: + libxcrypt-compat x86_64 4.4.36-1.fc38 updates 90 k + + Transaction Summary + ================================================================================ + Install 1 Package + Upgrade 14 Packages + + Total download size: 16 M + Downloading Packages: + (1/15): fedora-release-container-38-37.noarch.r 160 kB/s | 10 kB 00:00 + (2/15): fedora-release-common-38-37.noarch.rpm 228 kB/s | 20 kB 00:00 + (3/15): fedora-release-identity-container-38-37 385 kB/s | 12 kB 00:00 + (4/15): libxcrypt-compat-4.4.36-1.fc38.x86_64.r 648 kB/s | 90 kB 00:00 + (5/15): glibc-minimal-langpack-2.37-19.fc38.x86 1.1 MB/s | 42 kB 00:00 + (6/15): glibc-common-2.37-19.fc38.x86_64.rpm 2.4 MB/s | 320 kB 00:00 + (7/15): libnghttp2-1.52.0-3.fc38.x86_64.rpm 1.7 MB/s | 75 kB 00:00 + (8/15): glibc-2.37-19.fc38.x86_64.rpm 8.1 MB/s | 2.1 MB 00:00 + (9/15): python3-3.11.9-2.fc38.x86_64.rpm 686 kB/s | 28 kB 00:00 + (10/15): python-pip-wheel-22.3.1-4.fc38.noarch. 8.8 MB/s | 1.4 MB 00:00 + (11/15): tpm2-tss-4.0.2-1.fc38.x86_64.rpm 6.1 MB/s | 391 kB 00:00 + (12/15): gnutls-3.8.5-1.fc38.x86_64.rpm 3.1 MB/s | 1.1 MB 00:00 + (13/15): vim-data-9.1.393-1.fc38.noarch.rpm 503 kB/s | 23 kB 00:00 + (14/15): vim-minimal-9.1.393-1.fc38.x86_64.rpm 4.3 MB/s | 810 kB 00:00 + (15/15): python3-libs-3.11.9-2.fc38.x86_64.rpm 11 MB/s | 9.6 MB 00:00 + -------------------------------------------------------------------------------- + Total 11 MB/s | 16 MB 00:01 + Running transaction check + Transaction check succeeded. + Running transaction test + Transaction test succeeded. + Running transaction + Preparing : 1/1 + Upgrading : glibc-common-2.37-19.fc38.x86_64 1/29 + Upgrading : glibc-minimal-langpack-2.37-19.fc38.x86_64 2/29 + Running scriptlet: glibc-2.37-19.fc38.x86_64 3/29 + Upgrading : glibc-2.37-19.fc38.x86_64 3/29 + Running scriptlet: glibc-2.37-19.fc38.x86_64 3/29 + Upgrading : fedora-release-identity-container-38-37.noarch 4/29 + Upgrading : fedora-release-container-38-37.noarch 5/29 + Upgrading : fedora-release-common-38-37.noarch 6/29 + Installing : libxcrypt-compat-4.4.36-1.fc38.x86_64 7/29 + Upgrading : python-pip-wheel-22.3.1-4.fc38.noarch 8/29 + Upgrading : python3-3.11.9-2.fc38.x86_64 9/29 + Upgrading : python3-libs-3.11.9-2.fc38.x86_64 10/29 + Upgrading : vim-data-2:9.1.393-1.fc38.noarch 11/29 + Upgrading : vim-minimal-2:9.1.393-1.fc38.x86_64 12/29 + Upgrading : gnutls-3.8.5-1.fc38.x86_64 13/29 + Upgrading : libnghttp2-1.52.0-3.fc38.x86_64 14/29 + Running scriptlet: tpm2-tss-4.0.2-1.fc38.x86_64 15/29 + Upgrading : tpm2-tss-4.0.2-1.fc38.x86_64 15/29 + Cleanup : fedora-release-common-38-36.noarch 16/29 + Cleanup : gnutls-3.8.4-1.fc38.x86_64 17/29 + Cleanup : tpm2-tss-4.0.1-3.fc38.x86_64 18/29 + Cleanup : vim-minimal-2:9.1.309-1.fc38.x86_64 19/29 + Cleanup : libnghttp2-1.52.0-2.fc38.x86_64 20/29 + Cleanup : python3-3.11.8-2.fc38.x86_64 21/29 + Cleanup : fedora-release-container-38-36.noarch 22/29 + Cleanup : fedora-release-identity-container-38-36.noarch 23/29 + Cleanup : vim-data-2:9.1.309-1.fc38.noarch 24/29 + Cleanup : python3-libs-3.11.8-2.fc38.x86_64 25/29 + Cleanup : python-pip-wheel-22.3.1-3.fc38.noarch 26/29 + Cleanup : glibc-2.37-18.fc38.x86_64 27/29 + Cleanup : glibc-minimal-langpack-2.37-18.fc38.x86_64 28/29 + Cleanup : glibc-common-2.37-18.fc38.x86_64 29/29 + Running scriptlet: glibc-common-2.37-18.fc38.x86_64 29/29 + Verifying : libxcrypt-compat-4.4.36-1.fc38.x86_64 1/29 + Verifying : fedora-release-common-38-37.noarch 2/29 + Verifying : fedora-release-common-38-36.noarch 3/29 + Verifying : fedora-release-container-38-37.noarch 4/29 + Verifying : fedora-release-container-38-36.noarch 5/29 + Verifying : fedora-release-identity-container-38-37.noarch 6/29 + Verifying : fedora-release-identity-container-38-36.noarch 7/29 + Verifying : glibc-2.37-19.fc38.x86_64 8/29 + Verifying : glibc-2.37-18.fc38.x86_64 9/29 + Verifying : glibc-common-2.37-19.fc38.x86_64 10/29 + Verifying : glibc-common-2.37-18.fc38.x86_64 11/29 + Verifying : glibc-minimal-langpack-2.37-19.fc38.x86_64 12/29 + Verifying : glibc-minimal-langpack-2.37-18.fc38.x86_64 13/29 + Verifying : gnutls-3.8.5-1.fc38.x86_64 14/29 + Verifying : gnutls-3.8.4-1.fc38.x86_64 15/29 + Verifying : libnghttp2-1.52.0-3.fc38.x86_64 16/29 + Verifying : libnghttp2-1.52.0-2.fc38.x86_64 17/29 + Verifying : python-pip-wheel-22.3.1-4.fc38.noarch 18/29 + Verifying : python-pip-wheel-22.3.1-3.fc38.noarch 19/29 + Verifying : python3-3.11.9-2.fc38.x86_64 20/29 + Verifying : python3-3.11.8-2.fc38.x86_64 21/29 + Verifying : python3-libs-3.11.9-2.fc38.x86_64 22/29 + Verifying : python3-libs-3.11.8-2.fc38.x86_64 23/29 + Verifying : tpm2-tss-4.0.2-1.fc38.x86_64 24/29 + Verifying : tpm2-tss-4.0.1-3.fc38.x86_64 25/29 + Verifying : vim-data-2:9.1.393-1.fc38.noarch 26/29 + Verifying : vim-data-2:9.1.309-1.fc38.noarch 27/29 + Verifying : vim-minimal-2:9.1.393-1.fc38.x86_64 28/29 + Verifying : vim-minimal-2:9.1.309-1.fc38.x86_64 29/29 + + Upgraded: + fedora-release-common-38-37.noarch + fedora-release-container-38-37.noarch + fedora-release-identity-container-38-37.noarch + glibc-2.37-19.fc38.x86_64 + glibc-common-2.37-19.fc38.x86_64 + glibc-minimal-langpack-2.37-19.fc38.x86_64 + gnutls-3.8.5-1.fc38.x86_64 + libnghttp2-1.52.0-3.fc38.x86_64 + python-pip-wheel-22.3.1-4.fc38.noarch + python3-3.11.9-2.fc38.x86_64 + python3-libs-3.11.9-2.fc38.x86_64 + tpm2-tss-4.0.2-1.fc38.x86_64 + vim-data-2:9.1.393-1.fc38.noarch + vim-minimal-2:9.1.393-1.fc38.x86_64 + Installed: + libxcrypt-compat-4.4.36-1.fc38.x86_64 + + Complete! + 42 files removed + ==> aa51aa7: IMAGE /tmp/velocity/build/fedora-38-aa51aa7/aa51aa7.sif (fedora@38) BUILT [0:01:33] + + ==> BUILT: /tmp/velocity/fedora-38__x86_64-fedora.sif Adding Different Versions ######################### So now we have a base Fedora image. That's great but before we move on let's make some different versions of the image -so that we have more options for building later. Go ahead and copy the `fedora/38` directory several times: +so that we have more options for building later. Edit the fedora ``specs.yaml`` and add some versions. -.. code-block:: bash - - cp -rf fedora/38/ fedora/39 - cp -rf fedora/38/ fedora/40 - cp -rf fedora/38/ fedora/41 - -For each of the versions you will need to go in and change the tag on the source. For example `docker.io/fedora:38` -in `fedora/40/template/fedora.vtmp` should be changed to `docker.io/fedora:40`. +.. code-block:: yaml + :caption: spec.yaml -.. code-block:: bash - :caption: VELOCITY_IMAGE_DIR - - . - └── fedora - ├── 38 - │ ├── specifications.yaml - │ └── templates - │ └── fedora.vtmp - ├── 39 - │ ├── specifications.yaml - │ └── templates - │ └── fedora.vtmp - ├── 40 - │ ├── specifications.yaml - │ └── templates - │ └── fedora.vtmp - └── 41 - ├── specifications.yaml - └── templates - └── fedora.vtmp + versions: + - spec: + - 38 + - 39 + - 40 + - 41 + when: distro=fedora .. code-block:: bash user@hostname:~$ velocity avail - ==> System: x86_64 - ==> Backend: podman - ==> Distro: fedora - ==> fedora - 38 - 39 - 40 - 41 + 38 + 39 + 40 + 41 Specifying Version ################## -When building an image Velocity will default to the latest image. To specify a version use `@=` e.g. -fedora\@=40. Versions are compared by splitting the version string up based on periods and then comparing the sections -left to right alphanumerically. +When building an image Velocity will default to the latest image. To specify a version use ``@`` e.g. +``fedora@40``. Versions take the form ``..-``. You can also specify greater than, less +than, and in-between via ``@:``, ``@:`` and ``@:`` respectively. Hello World! ############ @@ -398,525 +264,256 @@ Now let's get a little more complicated. Let's create an image that runs a pytho can give it whatever version you want: .. code-block:: bash - :caption: VELOCITY_IMAGE_DIR - - . - ├── fedora - │ ├── 38 - │ │ ├── specifications.yaml - │ │ └── templates - │ │ └── fedora.vtmp - │ ├── 39 - │ │ ├── specifications.yaml - │ │ └── templates - │ │ └── fedora.vtmp - │ ├── 40 - │ │ ├── specifications.yaml - │ │ └── templates - │ │ └── fedora.vtmp - │ └── 41 - │ ├── specifications.yaml - │ └── templates - │ └── fedora.vtmp - └── hello-world - └── 1.0 - ├── x86_64 - │ └── hello_world.py - ├── specifications.yaml - └── templates - └── fedora.vtmp - -Notice that now there is a new folder called `x86_64` with a python file in it. + :caption: /tmp/velocity/images + + fedora + ├── specs.yaml + └── templates + └── default.vtmp + hello-world + ├── files + │ └── hello_world.py + ├── specs.yaml + └── templates + └── default.vtmp + +Notice that now there is a new folder called ``files`` with a python script in it. .. code-block:: bash - :caption: hello-world/1.0/x86_64/hello_world.py + :caption: hello_world.py #!/usr/bin/env python3 print("Hello, World!") .. code-block:: yaml - :caption: hello-world/1.0/specifications.yaml - - build_specifications: + :caption: specs.yaml - x86_64: - podman: - fedora: - dependencies: - - fedora - variables: - fr: fakeroot + versions: + - spec: 1.0 + dependencies: + - spec: fedora + when: distro=fedora + files: + - name: hello_world.py .. code-block:: text - :caption: hello-world/1.0/templates/fedora.vtmp + :caption: default.vtmp @from - %(__base__) + {{ __base__ }} @copy - hello_world.py /hello_world.py + hello_world.py /hello_world + + @run + dnf -y install python3 + chmod +x /hello_world @entry - /hello_world.py + /hello_world - @label - velocity.image.%(__name__)__%(__tag__) %(__hash__) .. code-block:: bash user@hostname:~$ velocity avail - ==> System: x86_64 - ==> Backend: podman - ==> Distro: fedora - ==> fedora - 38 - 39 - 40 - 41 + 38 + 39 + 40 + 41 ==> hello-world - 1.0 + 1.0 .. code-block:: bash user@hostname:~$ velocity build hello-world -v - ==> System: x86_64 - ==> Backend: podman - ==> Distro: fedora - ==> Build Order: - fedora@=41 - hello-world@=1.0 - - ==> wmrxxohy: BUILD fedora@=41 ... - ==> wmrxxohy: GENERATING SCRIPT ... - SCRIPT: /tmp/velocity/build/wmrxxohy/script - FROM docker.io/fedora:41 - - RUN dnf -y upgrade - - LABEL velocity.config.system="x86_64" \ - velocity.config.backend="podman" \ - velocity.config.distro="fedora" \ - velocity.image.fedora__41="1ad7926d7e542fa521fe4a2eca54aa73caea82958b1f07adf04728b5762063ac" - - ==> wmrxxohy: BUILDING ... - #!/usr/bin/env bash - podman build -f /tmp/velocity/build/wmrxxohy/script -t localhost/wmrxxohy:latest .; - STEP 1/3: FROM docker.io/fedora:41 - STEP 2/3: RUN dnf -y upgrade - Fedora rawhide openh264 (From Cisco) - x86_64 59 B/s | 123 B 00:02 - Fedora - Rawhide - Developmental packages for t 3.1 MB/s | 20 MB 00:06 - Dependencies resolved. - ====================================================================================== - Package Arch Version Repo Size - ====================================================================================== - Upgrading: - audit-libs x86_64 4.0.1-1.fc41 rawhide 126 k - authselect x86_64 1.5.0-5.fc41 rawhide 146 k - authselect-libs x86_64 1.5.0-5.fc41 rawhide 219 k - crypto-policies noarch 20240320-1.git58e3d95.fc41 rawhide 91 k - curl x86_64 8.6.0-7.fc41 rawhide 301 k - dnf noarch 4.19.2-1.fc41 rawhide 503 k - dnf-data noarch 4.19.2-1.fc41 rawhide 40 k - elfutils-default-yama-scope noarch 0.191-5.fc41 rawhide 13 k - elfutils-libelf x86_64 0.191-5.fc41 rawhide 209 k - elfutils-libs x86_64 0.191-5.fc41 rawhide 258 k - expat x86_64 2.6.2-1.fc41 rawhide 113 k - fedora-release-common noarch 41-0.6 rawhide 21 k - fedora-release-container noarch 41-0.6 rawhide 11 k - fedora-release-identity-container noarch 41-0.6 rawhide 12 k - glib2 x86_64 2.80.0-1.fc41 rawhide 3.0 M - glibc x86_64 2.39.9000-10.fc41 rawhide 2.2 M - glibc-common x86_64 2.39.9000-10.fc41 rawhide 393 k - glibc-minimal-langpack x86_64 2.39.9000-10.fc41 rawhide 106 k - gmp x86_64 1:6.3.0-1.fc41 rawhide 317 k - gnupg2 x86_64 2.4.5-1.fc41 rawhide 2.7 M - gnutls x86_64 3.8.4-1.fc41 rawhide 1.1 M - libassuan x86_64 2.5.7-1.fc41 rawhide 67 k - libblkid x86_64 2.40-0.12.fc41 rawhide 125 k - libcurl x86_64 8.6.0-7.fc41 rawhide 345 k - libdnf x86_64 0.73.1-1.fc41 rawhide 697 k - libeconf x86_64 0.6.2-1.fc41 rawhide 32 k - libffi x86_64 3.4.6-1.fc41 rawhide 40 k - libgcc x86_64 14.0.1-0.13.fc41 rawhide 123 k - libgcrypt x86_64 1.10.3-4.fc41 rawhide 504 k - libgomp x86_64 14.0.1-0.13.fc41 rawhide 344 k - libgpg-error x86_64 1.48-1.fc41 rawhide 232 k - libksba x86_64 1.6.6-1.fc41 rawhide 159 k - libmodulemd x86_64 2.15.0-9.fc41 rawhide 233 k - libmount x86_64 2.40-0.12.fc41 rawhide 155 k - libnghttp2 x86_64 1.60.0-2.fc41 rawhide 76 k - librepo x86_64 1.17.1-1.fc41 rawhide 99 k - libreport-filesystem noarch 2.17.15-1.fc41 rawhide 14 k - libsmartcols x86_64 2.40-0.12.fc41 rawhide 84 k - libssh x86_64 0.10.6-6.fc41 rawhide 212 k - libssh-config noarch 0.10.6-6.fc41 rawhide 9.1 k - libstdc++ x86_64 14.0.1-0.13.fc41 rawhide 881 k - libtirpc x86_64 1.3.4-1.rc3.fc41 rawhide 92 k - libunistring x86_64 1.1-7.fc41 rawhide 545 k - libuuid x86_64 2.40-0.12.fc41 rawhide 29 k - libxml2 x86_64 2.12.6-1.fc41 rawhide 686 k - libzstd x86_64 1.5.6-1.fc41 rawhide 309 k - npth x86_64 1.7-1.fc41 rawhide 25 k - openssl-libs x86_64 1:3.2.1-3.fc41 rawhide 2.3 M - pcre2 x86_64 10.43-1.fc41 rawhide 242 k - pcre2-syntax noarch 10.43-1.fc41 rawhide 149 k - python-pip-wheel noarch 24.0-2.fc41 rawhide 1.5 M - python3 x86_64 3.12.2-3.fc41 rawhide 27 k - python3-dnf noarch 4.19.2-1.fc41 rawhide 594 k - python3-hawkey x86_64 0.73.1-1.fc41 rawhide 105 k - python3-libdnf x86_64 0.73.1-1.fc41 rawhide 847 k - python3-libs x86_64 3.12.2-3.fc41 rawhide 9.1 M - shadow-utils x86_64 2:4.15.1-2.fc41 rawhide 1.3 M - sqlite-libs x86_64 3.45.2-1.fc41 rawhide 706 k - systemd-libs x86_64 255.4-1.fc41 rawhide 708 k - tzdata noarch 2024a-4.fc41 rawhide 716 k - util-linux-core x86_64 2.40-0.12.fc41 rawhide 537 k - vim-data noarch 2:9.1.181-1.fc41 rawhide 23 k - vim-minimal x86_64 2:9.1.181-1.fc41 rawhide 807 k - xz-libs x86_64 1:5.4.6-3.fc41 rawhide 110 k - yum noarch 4.19.2-1.fc41 rawhide 37 k - - Transaction Summary - ====================================================================================== - Upgrade 65 Packages - - Total download size: 38 M - Downloading Packages: - (1/65): audit-libs-4.0.1-1.fc41.x86_64.rpm 136 kB/s | 126 kB 00:00 - (2/65): authselect-1.5.0-5.fc41.x86_64.rpm 153 kB/s | 146 kB 00:00 - (3/65): crypto-policies-20240320-1.git58e3d95.f 623 kB/s | 91 kB 00:00 - (4/65): authselect-libs-1.5.0-5.fc41.x86_64.rpm 199 kB/s | 219 kB 00:01 - [MIRROR] dnf-4.19.2-1.fc41.noarch.rpm: Status code: 404 for http://mirror.math.princeton.edu/pub/fedora/linux/development/rawhide/Everything/x86_64/os/Packages/d/dnf-4.19.2-1.fc41.noarch.rpm (IP: 128.112.18.21) - [MIRROR] dnf-data-4.19.2-1.fc41.noarch.rpm: Status code: 404 for http://mirror.math.princeton.edu/pub/fedora/linux/development/rawhide/Everything/x86_64/os/Packages/d/dnf-data-4.19.2-1.fc41.noarch.rpm (IP: 128.112.18.21) - (5/65): curl-8.6.0-7.fc41.x86_64.rpm 919 kB/s | 301 kB 00:00 - (6/65): elfutils-default-yama-scope-0.191-5.fc4 105 kB/s | 13 kB 00:00 - (7/65): elfutils-libelf-0.191-5.fc41.x86_64.rpm 739 kB/s | 209 kB 00:00 - (8/65): dnf-data-4.19.2-1.fc41.noarch.rpm 51 kB/s | 40 kB 00:00 - (9/65): elfutils-libs-0.191-5.fc41.x86_64.rpm 980 kB/s | 258 kB 00:00 - (10/65): expat-2.6.2-1.fc41.x86_64.rpm 787 kB/s | 113 kB 00:00 - (11/65): fedora-release-common-41-0.6.noarch.rp 165 kB/s | 21 kB 00:00 - (12/65): fedora-release-container-41-0.6.noarch 87 kB/s | 11 kB 00:00 - (13/65): fedora-release-identity-container-41-0 77 kB/s | 12 kB 00:00 - (14/65): dnf-4.19.2-1.fc41.noarch.rpm 402 kB/s | 503 kB 00:01 - (15/65): glib2-2.80.0-1.fc41.x86_64.rpm 4.8 MB/s | 3.0 MB 00:00 - (16/65): glibc-minimal-langpack-2.39.9000-10.fc 783 kB/s | 106 kB 00:00 - (17/65): gmp-6.3.0-1.fc41.x86_64.rpm 1.7 MB/s | 317 kB 00:00 - (18/65): glibc-common-2.39.9000-10.fc41.x86_64. 507 kB/s | 393 kB 00:00 - (19/65): gnupg2-2.4.5-1.fc41.x86_64.rpm 8.2 MB/s | 2.7 MB 00:00 - (20/65): libassuan-2.5.7-1.fc41.x86_64.rpm 458 kB/s | 67 kB 00:00 - (21/65): libblkid-2.40-0.12.fc41.x86_64.rpm 687 kB/s | 125 kB 00:00 - (22/65): glibc-2.39.9000-10.fc41.x86_64.rpm 1.0 MB/s | 2.2 MB 00:02 - [MIRROR] libdnf-0.73.1-1.fc41.x86_64.rpm: Status code: 404 for http://mirror.math.princeton.edu/pub/fedora/linux/development/rawhide/Everything/x86_64/os/Packages/l/libdnf-0.73.1-1.fc41.x86_64.rpm (IP: 128.112.18.21) - (23/65): gnutls-3.8.4-1.fc41.x86_64.rpm 668 kB/s | 1.1 MB 00:01 - (24/65): libeconf-0.6.2-1.fc41.x86_64.rpm 249 kB/s | 32 kB 00:00 - (25/65): libdnf-0.73.1-1.fc41.x86_64.rpm 1.6 MB/s | 697 kB 00:00 - (26/65): libffi-3.4.6-1.fc41.x86_64.rpm 205 kB/s | 40 kB 00:00 - (27/65): libgcc-14.0.1-0.13.fc41.x86_64.rpm 391 kB/s | 123 kB 00:00 - (28/65): libgomp-14.0.1-0.13.fc41.x86_64.rpm 642 kB/s | 344 kB 00:00 - (29/65): libgcrypt-1.10.3-4.fc41.x86_64.rpm 613 kB/s | 504 kB 00:00 - (30/65): libgpg-error-1.48-1.fc41.x86_64.rpm 579 kB/s | 232 kB 00:00 - (31/65): libksba-1.6.6-1.fc41.x86_64.rpm 580 kB/s | 159 kB 00:00 - (32/65): libcurl-8.6.0-7.fc41.x86_64.rpm 140 kB/s | 345 kB 00:02 - (33/65): libmodulemd-2.15.0-9.fc41.x86_64.rpm 406 kB/s | 233 kB 00:00 - (34/65): libmount-2.40-0.12.fc41.x86_64.rpm 271 kB/s | 155 kB 00:00 - (35/65): libreport-filesystem-2.17.15-1.fc41.no 82 kB/s | 14 kB 00:00 - (36/65): librepo-1.17.1-1.fc41.x86_64.rpm 501 kB/s | 99 kB 00:00 - (37/65): libsmartcols-2.40-0.12.fc41.x86_64.rpm 323 kB/s | 84 kB 00:00 - (38/65): libssh-0.10.6-6.fc41.x86_64.rpm 744 kB/s | 212 kB 00:00 - (39/65): libssh-config-0.10.6-6.fc41.noarch.rpm 74 kB/s | 9.1 kB 00:00 - (40/65): libtirpc-1.3.4-1.rc3.fc41.x86_64.rpm 357 kB/s | 92 kB 00:00 - (41/65): libstdc++-14.0.1-0.13.fc41.x86_64.rpm 837 kB/s | 881 kB 00:01 - (42/65): libnghttp2-1.60.0-2.fc41.x86_64.rpm 35 kB/s | 76 kB 00:02 - (43/65): libuuid-2.40-0.12.fc41.x86_64.rpm 208 kB/s | 29 kB 00:00 - (44/65): libunistring-1.1-7.fc41.x86_64.rpm 534 kB/s | 545 kB 00:01 - (45/65): npth-1.7-1.fc41.x86_64.rpm 187 kB/s | 25 kB 00:00 - (46/65): libzstd-1.5.6-1.fc41.x86_64.rpm 767 kB/s | 309 kB 00:00 - (47/65): pcre2-10.43-1.fc41.x86_64.rpm 611 kB/s | 242 kB 00:00 - (48/65): pcre2-syntax-10.43-1.fc41.noarch.rpm 386 kB/s | 149 kB 00:00 - (49/65): python-pip-wheel-24.0-2.fc41.noarch.rp 640 kB/s | 1.5 MB 00:02 - (50/65): python3-3.12.2-3.fc41.x86_64.rpm 203 kB/s | 27 kB 00:00 - [MIRROR] python3-dnf-4.19.2-1.fc41.noarch.rpm: Status code: 404 for http://mirror.math.princeton.edu/pub/fedora/linux/development/rawhide/Everything/x86_64/os/Packages/p/python3-dnf-4.19.2-1.fc41.noarch.rpm (IP: 128.112.18.21) - (51/65): libxml2-2.12.6-1.fc41.x86_64.rpm 168 kB/s | 686 kB 00:04 - (52/65): openssl-libs-3.2.1-3.fc41.x86_64.rpm 554 kB/s | 2.3 MB 00:04 - (53/65): python3-hawkey-0.73.1-1.fc41.x86_64.rp 130 kB/s | 105 kB 00:00 - (54/65): python3-dnf-4.19.2-1.fc41.noarch.rpm 425 kB/s | 594 kB 00:01 - (55/65): shadow-utils-4.15.1-2.fc41.x86_64.rpm 3.5 MB/s | 1.3 MB 00:00 - (56/65): sqlite-libs-3.45.2-1.fc41.x86_64.rpm 2.8 MB/s | 706 kB 00:00 - (57/65): python3-libdnf-0.73.1-1.fc41.x86_64.rp 678 kB/s | 847 kB 00:01 - (58/65): systemd-libs-255.4-1.fc41.x86_64.rpm 2.9 MB/s | 708 kB 00:00 - (59/65): util-linux-core-2.40-0.12.fc41.x86_64. 3.0 MB/s | 537 kB 00:00 - (60/65): tzdata-2024a-4.fc41.noarch.rpm 1.5 MB/s | 716 kB 00:00 - (61/65): vim-data-9.1.181-1.fc41.noarch.rpm 125 kB/s | 23 kB 00:00 - (62/65): xz-libs-5.4.6-3.fc41.x86_64.rpm 530 kB/s | 110 kB 00:00 - (63/65): yum-4.19.2-1.fc41.noarch.rpm 215 kB/s | 37 kB 00:00 - (64/65): python3-libs-3.12.2-3.fc41.x86_64.rpm 3.5 MB/s | 9.1 MB 00:02 - (65/65): vim-minimal-9.1.181-1.fc41.x86_64.rpm 218 kB/s | 807 kB 00:03 - -------------------------------------------------------------------------------- - Total 2.0 MB/s | 38 MB 00:19 - Running transaction check - Transaction check succeeded. - Running transaction test - Transaction test succeeded. - Running transaction - Preparing : 1/1 - Upgrading : libgcc-14.0.1-0.13.fc41.x86_64 1/130 - Running scriptlet: libgcc-14.0.1-0.13.fc41.x86_64 1/130 - Upgrading : tzdata-2024a-4.fc41.noarch 2/130 - Upgrading : crypto-policies-20240320-1.git58e3d95.fc41.noarc 3/130 - Running scriptlet: crypto-policies-20240320-1.git58e3d95.fc41.noarc 3/130 - Upgrading : glibc-common-2.39.9000-10.fc41.x86_64 4/130 - Upgrading : glibc-minimal-langpack-2.39.9000-10.fc41.x86_64 5/130 - Running scriptlet: glibc-2.39.9000-10.fc41.x86_64 6/130 - Upgrading : glibc-2.39.9000-10.fc41.x86_64 6/130 - Running scriptlet: glibc-2.39.9000-10.fc41.x86_64 6/130 - Upgrading : libgpg-error-1.48-1.fc41.x86_64 7/130 - Upgrading : libuuid-2.40-0.12.fc41.x86_64 8/130 - Upgrading : openssl-libs-1:3.2.1-3.fc41.x86_64 9/130 - Upgrading : xz-libs-1:5.4.6-3.fc41.x86_64 10/130 - Upgrading : libsmartcols-2.40-0.12.fc41.x86_64 11/130 - Upgrading : libstdc++-14.0.1-0.13.fc41.x86_64 12/130 - Upgrading : libzstd-1.5.6-1.fc41.x86_64 13/130 - Upgrading : sqlite-libs-3.45.2-1.fc41.x86_64 14/130 - Upgrading : libblkid-2.40-0.12.fc41.x86_64 15/130 - Upgrading : libmount-2.40-0.12.fc41.x86_64 16/130 - Upgrading : libffi-3.4.6-1.fc41.x86_64 17/130 - Upgrading : fedora-release-identity-container-41-0.6.noarch 18/130 - Upgrading : fedora-release-container-41-0.6.noarch 19/130 - Upgrading : fedora-release-common-41-0.6.noarch 20/130 - Upgrading : elfutils-libelf-0.191-5.fc41.x86_64 21/130 - Upgrading : systemd-libs-255.4-1.fc41.x86_64 22/130 - Upgrading : libxml2-2.12.6-1.fc41.x86_64 23/130 - Upgrading : libassuan-2.5.7-1.fc41.x86_64 24/130 - Upgrading : libgcrypt-1.10.3-4.fc41.x86_64 25/130 - Upgrading : libksba-1.6.6-1.fc41.x86_64 26/130 - Upgrading : audit-libs-4.0.1-1.fc41.x86_64 27/130 - Upgrading : authselect-libs-1.5.0-5.fc41.x86_64 28/130 - Upgrading : expat-2.6.2-1.fc41.x86_64 29/130 - Upgrading : gmp-1:6.3.0-1.fc41.x86_64 30/130 - Upgrading : libeconf-0.6.2-1.fc41.x86_64 31/130 - Upgrading : libnghttp2-1.60.0-2.fc41.x86_64 32/130 - Upgrading : libtirpc-1.3.4-1.rc3.fc41.x86_64 33/130 - Upgrading : libunistring-1.1-7.fc41.x86_64 34/130 - Upgrading : gnutls-3.8.4-1.fc41.x86_64 35/130 - Upgrading : npth-1.7-1.fc41.x86_64 36/130 - Upgrading : vim-data-2:9.1.181-1.fc41.noarch 37/130 - Upgrading : python-pip-wheel-24.0-2.fc41.noarch 38/130 - Upgrading : python3-3.12.2-3.fc41.x86_64 39/130 - Upgrading : python3-libs-3.12.2-3.fc41.x86_64 40/130 - Upgrading : pcre2-syntax-10.43-1.fc41.noarch 41/130 - Upgrading : pcre2-10.43-1.fc41.x86_64 42/130 - Upgrading : glib2-2.80.0-1.fc41.x86_64 43/130 - Upgrading : libmodulemd-2.15.0-9.fc41.x86_64 44/130 - Upgrading : libssh-config-0.10.6-6.fc41.noarch 45/130 - Upgrading : libssh-0.10.6-6.fc41.x86_64 46/130 - Upgrading : libcurl-8.6.0-7.fc41.x86_64 47/130 - Upgrading : librepo-1.17.1-1.fc41.x86_64 48/130 - Upgrading : libdnf-0.73.1-1.fc41.x86_64 49/130 - Upgrading : python3-libdnf-0.73.1-1.fc41.x86_64 50/130 - Upgrading : python3-hawkey-0.73.1-1.fc41.x86_64 51/130 - Upgrading : libreport-filesystem-2.17.15-1.fc41.noarch 52/130 - Upgrading : dnf-data-4.19.2-1.fc41.noarch 53/130 - Upgrading : python3-dnf-4.19.2-1.fc41.noarch 54/130 - Upgrading : dnf-4.19.2-1.fc41.noarch 55/130 - Running scriptlet: dnf-4.19.2-1.fc41.noarch 55/130 - Upgrading : elfutils-default-yama-scope-0.191-5.fc41.noarch 56/130 - Running scriptlet: elfutils-default-yama-scope-0.191-5.fc41.noarch 56/130 - Upgrading : elfutils-libs-0.191-5.fc41.x86_64 57/130 - Upgrading : yum-4.19.2-1.fc41.noarch 58/130 - Upgrading : curl-8.6.0-7.fc41.x86_64 59/130 - Upgrading : vim-minimal-2:9.1.181-1.fc41.x86_64 60/130 - Upgrading : gnupg2-2.4.5-1.fc41.x86_64 61/130 - Upgrading : shadow-utils-2:4.15.1-2.fc41.x86_64 62/130 - Upgrading : authselect-1.5.0-5.fc41.x86_64 63/130 - Upgrading : util-linux-core-2.40-0.12.fc41.x86_64 64/130 - Upgrading : libgomp-14.0.1-0.13.fc41.x86_64 65/130 - Cleanup : util-linux-core-2.40-0.9.rc1.fc41.x86_64 66/130 - Cleanup : systemd-libs-255.3-1.fc40.x86_64 67/130 - Cleanup : gnupg2-2.4.4-1.fc40.x86_64 68/130 - Cleanup : elfutils-libs-0.190-6.fc40.x86_64 69/130 - Cleanup : shadow-utils-2:4.14.0-6.fc40.x86_64 70/130 - Cleanup : vim-minimal-2:9.1.113-1.fc41.x86_64 71/130 - Running scriptlet: authselect-1.5.0-3.fc40.x86_64 72/130 - Cleanup : authselect-1.5.0-3.fc40.x86_64 72/130 - Cleanup : curl-8.6.0-6.fc40.x86_64 73/130 - Cleanup : fedora-release-common-41-0.1.noarch 74/130 - Cleanup : libgcrypt-1.10.3-3.fc40.x86_64 75/130 - Cleanup : elfutils-libelf-0.190-6.fc40.x86_64 76/130 - Cleanup : libassuan-2.5.6-4.fc40.x86_64 77/130 - Cleanup : authselect-libs-1.5.0-3.fc40.x86_64 78/130 - Cleanup : audit-libs-4.0-8.fc40.x86_64 79/130 - Cleanup : libksba-1.6.5-3.fc40.x86_64 80/130 - Cleanup : libgpg-error-1.47-4.fc40.x86_64 81/130 - Cleanup : libeconf-0.5.2-3.fc40.x86_64 82/130 - Cleanup : libgomp-14.0.1-0.6.fc40.x86_64 83/130 - Cleanup : fedora-release-container-41-0.1.noarch 84/130 - Cleanup : yum-4.19.0-1.fc40.noarch 85/130 - Cleanup : libzstd-1.5.5-5.fc40.x86_64 86/130 - Cleanup : npth-1.6-18.fc40.x86_64 87/130 - Running scriptlet: dnf-4.19.0-1.fc40.noarch 88/130 - Cleanup : dnf-4.19.0-1.fc40.noarch 88/130 - Running scriptlet: dnf-4.19.0-1.fc40.noarch 88/130 - Cleanup : python3-dnf-4.19.0-1.fc40.noarch 89/130 - Cleanup : dnf-data-4.19.0-1.fc40.noarch 90/130 - Cleanup : libreport-filesystem-2.17.14-1.fc40.noarch 91/130 - Cleanup : fedora-release-identity-container-41-0.1.noarch 92/130 - Cleanup : vim-data-2:9.1.113-1.fc41.noarch 93/130 - Cleanup : elfutils-default-yama-scope-0.190-6.fc40.noarch 94/130 - Cleanup : python3-hawkey-0.73.0-1.fc40.x86_64 95/130 - Cleanup : python3-libdnf-0.73.0-1.fc40.x86_64 96/130 - Cleanup : python3-libs-3.12.2-1.fc40.x86_64 97/130 - Cleanup : python3-3.12.2-1.fc40.x86_64 98/130 - Cleanup : libdnf-0.73.0-1.fc40.x86_64 99/130 - Cleanup : python-pip-wheel-23.3.2-1.fc40.noarch 100/130 - Cleanup : libstdc++-14.0.1-0.6.fc40.x86_64 101/130 - Cleanup : librepo-1.17.0-3.fc40.x86_64 102/130 - Cleanup : libcurl-8.6.0-6.fc40.x86_64 103/130 - Cleanup : libxml2-2.12.5-1.fc40.x86_64 104/130 - Cleanup : libssh-0.10.6-4.fc40.x86_64 105/130 - Cleanup : openssl-libs-1:3.2.1-2.fc40.x86_64 106/130 - Cleanup : sqlite-libs-3.45.1-2.fc40.x86_64 107/130 - Cleanup : libtirpc-1.3.4-1.rc2.fc40.2.x86_64 108/130 - Cleanup : libmodulemd-2.15.0-8.fc40.x86_64 109/130 - Cleanup : glib2-2.79.1-1.fc40.x86_64 110/130 - Cleanup : libmount-2.40-0.9.rc1.fc41.x86_64 111/130 - Cleanup : gnutls-3.8.3-2.fc40.x86_64 112/130 - Cleanup : libblkid-2.40-0.9.rc1.fc41.x86_64 113/130 - Cleanup : libuuid-2.40-0.9.rc1.fc41.x86_64 114/130 - Cleanup : xz-libs-5.4.6-1.fc40.x86_64 115/130 - Cleanup : libsmartcols-2.40-0.9.rc1.fc41.x86_64 116/130 - Cleanup : expat-2.6.0-1.fc41.x86_64 117/130 - Cleanup : gmp-1:6.2.1-8.fc40.x86_64 118/130 - Cleanup : libunistring-1.1-7.fc40.x86_64 119/130 - Cleanup : libffi-3.4.4-7.fc40.x86_64 120/130 - Cleanup : pcre2-10.42-2.fc40.2.x86_64 121/130 - Cleanup : libnghttp2-1.59.0-2.fc40.x86_64 122/130 - Cleanup : pcre2-syntax-10.42-2.fc40.2.noarch 123/130 - Cleanup : crypto-policies-20240201-1.git9f501f3.fc40.noarc 124/130 - Cleanup : libssh-config-0.10.6-4.fc40.noarch 125/130 - Cleanup : glibc-2.39.9000-1.fc41.x86_64 126/130 - Cleanup : glibc-minimal-langpack-2.39.9000-1.fc41.x86_64 127/130 - Cleanup : glibc-common-2.39.9000-1.fc41.x86_64 128/130 - Cleanup : tzdata-2024a-2.fc40.noarch 129/130 - Cleanup : libgcc-14.0.1-0.6.fc40.x86_64 130/130 - Running scriptlet: libgcc-14.0.1-0.6.fc40.x86_64 130/130 - Running scriptlet: authselect-libs-1.5.0-5.fc41.x86_64 130/130 - Running scriptlet: libgcc-14.0.1-0.6.fc40.x86_64 130/130 - - Upgraded: - audit-libs-4.0.1-1.fc41.x86_64 - authselect-1.5.0-5.fc41.x86_64 - authselect-libs-1.5.0-5.fc41.x86_64 - crypto-policies-20240320-1.git58e3d95.fc41.noarch - curl-8.6.0-7.fc41.x86_64 - dnf-4.19.2-1.fc41.noarch - dnf-data-4.19.2-1.fc41.noarch - elfutils-default-yama-scope-0.191-5.fc41.noarch - elfutils-libelf-0.191-5.fc41.x86_64 - elfutils-libs-0.191-5.fc41.x86_64 - expat-2.6.2-1.fc41.x86_64 - fedora-release-common-41-0.6.noarch - fedora-release-container-41-0.6.noarch - fedora-release-identity-container-41-0.6.noarch - glib2-2.80.0-1.fc41.x86_64 - glibc-2.39.9000-10.fc41.x86_64 - glibc-common-2.39.9000-10.fc41.x86_64 - glibc-minimal-langpack-2.39.9000-10.fc41.x86_64 - gmp-1:6.3.0-1.fc41.x86_64 - gnupg2-2.4.5-1.fc41.x86_64 - gnutls-3.8.4-1.fc41.x86_64 - libassuan-2.5.7-1.fc41.x86_64 - libblkid-2.40-0.12.fc41.x86_64 - libcurl-8.6.0-7.fc41.x86_64 - libdnf-0.73.1-1.fc41.x86_64 - libeconf-0.6.2-1.fc41.x86_64 - libffi-3.4.6-1.fc41.x86_64 - libgcc-14.0.1-0.13.fc41.x86_64 - libgcrypt-1.10.3-4.fc41.x86_64 - libgomp-14.0.1-0.13.fc41.x86_64 - libgpg-error-1.48-1.fc41.x86_64 - libksba-1.6.6-1.fc41.x86_64 - libmodulemd-2.15.0-9.fc41.x86_64 - libmount-2.40-0.12.fc41.x86_64 - libnghttp2-1.60.0-2.fc41.x86_64 - librepo-1.17.1-1.fc41.x86_64 - libreport-filesystem-2.17.15-1.fc41.noarch - libsmartcols-2.40-0.12.fc41.x86_64 - libssh-0.10.6-6.fc41.x86_64 - libssh-config-0.10.6-6.fc41.noarch - libstdc++-14.0.1-0.13.fc41.x86_64 - libtirpc-1.3.4-1.rc3.fc41.x86_64 - libunistring-1.1-7.fc41.x86_64 - libuuid-2.40-0.12.fc41.x86_64 - libxml2-2.12.6-1.fc41.x86_64 - libzstd-1.5.6-1.fc41.x86_64 - npth-1.7-1.fc41.x86_64 - openssl-libs-1:3.2.1-3.fc41.x86_64 - pcre2-10.43-1.fc41.x86_64 - pcre2-syntax-10.43-1.fc41.noarch - python-pip-wheel-24.0-2.fc41.noarch - python3-3.12.2-3.fc41.x86_64 - python3-dnf-4.19.2-1.fc41.noarch - python3-hawkey-0.73.1-1.fc41.x86_64 - python3-libdnf-0.73.1-1.fc41.x86_64 - python3-libs-3.12.2-3.fc41.x86_64 - shadow-utils-2:4.15.1-2.fc41.x86_64 - sqlite-libs-3.45.2-1.fc41.x86_64 - systemd-libs-255.4-1.fc41.x86_64 - tzdata-2024a-4.fc41.noarch - util-linux-core-2.40-0.12.fc41.x86_64 - vim-data-2:9.1.181-1.fc41.noarch - vim-minimal-2:9.1.181-1.fc41.x86_64 - xz-libs-1:5.4.6-3.fc41.x86_64 - yum-4.19.2-1.fc41.noarch - - Complete! - --> 43089713ea9 - STEP 3/3: LABEL velocity.config.system="x86_64" velocity.config.backend="podman" velocity.config.distro="fedora" velocity.image.fedora__41="1ad7926d7e542fa521fe4a2eca54aa73caea82958b1f07adf04728b5762063ac" - COMMIT localhost/wmrxxohy:latest - --> 49321c240b5 - Successfully tagged localhost/wmrxxohy:latest - 49321c240b522a0d66cd30b62addc99e87b5861e2e6c27f6d0136968c73be5aa - ==> wmrxxohy: IMAGE localhost/wmrxxohy:latest (fedora@=41) BUILT [0:01:02] - - ==> bdvdbcor: BUILD hello-world@=1.0 ... - ==> bdvdbcor: COPYING FILES ... - FILE: /home/xjv/tmp/hello-world/1.0/x86_64/hello_world.py -> /tmp/velocity/build/bdvdbcor/hello_world.py - ==> bdvdbcor: GENERATING SCRIPT ... - SCRIPT: /tmp/velocity/build/bdvdbcor/script - FROM localhost/wmrxxohy:latest - - COPY hello_world.py /hello_world.py - - LABEL velocity.image.hello-world__1.0="1b080f458c34ae2759d1eb3e8464d3d508e3bcb981a476f70709b0f20b6218bb" - - ENTRYPOINT ['/hello_world.py'] - - ==> bdvdbcor: BUILDING ... - #!/usr/bin/env bash - podman build -f /tmp/velocity/build/bdvdbcor/script -t localhost/hello-world__1.0__x86_64__fedora:latest .; - STEP 1/4: FROM localhost/wmrxxohy:latest - STEP 2/4: COPY hello_world.py /hello_world.py - --> fa1896e8689 - STEP 3/4: LABEL velocity.image.hello-world__1.0="1b080f458c34ae2759d1eb3e8464d3d508e3bcb981a476f70709b0f20b6218bb" - --> d58637575df - STEP 4/4: ENTRYPOINT ['/hello_world.py'] - COMMIT localhost/hello-world__1.0__x86_64__fedora:latest - --> 21211d9de40 - Successfully tagged localhost/hello-world__1.0__x86_64__fedora:latest - 21211d9de40aa6c0cb6b625e6f4fed265c88f9a4be09c4edcd93a78360580ecf - ==> bdvdbcor: IMAGE localhost/hello-world__1.0__x86_64__fedora:latest (hello-world@=1.0) BUILT [0:00:03] + fedora@41-8a9a360 + hello-world@1.0-de9c02b + + ==> 8a9a360: BUILD fedora@41 ... + ==> 8a9a360: GENERATING SCRIPT ... + SCRIPT: /tmp/velocity/build/fedora-41-8a9a360/script + Bootstrap: docker + From: docker.io/fedora:41 + + %post + dnf -y upgrade + dnf clean all + ==> 8a9a360: BUILDING ... + #!/usr/bin/env bash + apptainer build --disable-cache /tmp/velocity/build/fedora-41-8a9a360/8a9a360.sif /tmp/velocity/build/fedora-41-8a9a360/script; + Updating and loading repositories: + Fedora 41 openh264 (From Cisco) - x86_ 100% | 6.5 KiB/s | 4.8 KiB | 00m01s + Fedora 41 - x86_64 - Test Updates 100% | 1.8 MiB/s | 3.4 MiB | 00m02s + Fedora 41 - x86_64 100% | 13.9 MiB/s | 35.9 MiB | 00m03s + Fedora 41 - x86_64 - Updates 100% | 50.3 KiB/s | 31.9 KiB | 00m01s + Repositories loaded. + Package Arch Version Repository Size + Upgrading: + libgcc x86_64 14.2.1-3.fc41 updates-testing 274.6 KiB + replacing libgcc x86_64 14.2.1-1.fc41 34086d2996104518800c8d7dcc6139a1 274.6 KiB + libgomp x86_64 14.2.1-3.fc41 updates-testing 523.5 KiB + replacing libgomp x86_64 14.2.1-1.fc41 34086d2996104518800c8d7dcc6139a1 523.4 KiB + libstdc++ x86_64 14.2.1-3.fc41 updates-testing 2.8 MiB + replacing libstdc++ x86_64 14.2.1-1.fc41 34086d2996104518800c8d7dcc6139a1 2.8 MiB + openssl-libs x86_64 1:3.2.2-7.fc41 updates-testing 7.8 MiB + replacing openssl-libs x86_64 1:3.2.2-5.fc41 34086d2996104518800c8d7dcc6139a1 7.8 MiB + rpm x86_64 4.19.94-1.fc41 updates-testing 3.1 MiB + replacing rpm x86_64 4.19.92-6.fc41 34086d2996104518800c8d7dcc6139a1 3.1 MiB + rpm-build-libs x86_64 4.19.94-1.fc41 updates-testing 206.7 KiB + replacing rpm-build-libs x86_64 4.19.92-6.fc41 34086d2996104518800c8d7dcc6139a1 206.7 KiB + rpm-libs x86_64 4.19.94-1.fc41 updates-testing 721.9 KiB + replacing rpm-libs x86_64 4.19.92-6.fc41 34086d2996104518800c8d7dcc6139a1 721.9 KiB + systemd-libs x86_64 256.6-1.fc41 updates-testing 2.0 MiB + replacing systemd-libs x86_64 256.5-1.fc41 34086d2996104518800c8d7dcc6139a1 2.0 MiB + zlib-ng-compat x86_64 2.1.7-3.fc41 updates-testing 134.0 KiB + replacing zlib-ng-compat x86_64 2.1.7-2.fc41 34086d2996104518800c8d7dcc6139a1 134.0 KiB + + Transaction Summary: + Upgrading: 9 packages + Replacing: 9 packages + + Total size of inbound packages is 5 MiB. Need to download 5 MiB. + After this operation 2 KiB will be used (install 17 MiB, remove 17 MiB). + [1/9] libgcc-0:14.2.1-3.fc41.x86_64 100% | 132.2 KiB/s | 133.3 KiB | 00m01s + [2/9] libstdc++-0:14.2.1-3.fc41.x86_64 100% | 583.3 KiB/s | 887.8 KiB | 00m02s + [3/9] libgomp-0:14.2.1-3.fc41.x86_64 100% | 208.5 KiB/s | 354.1 KiB | 00m02s + [4/9] rpm-0:4.19.94-1.fc41.x86_64 100% | 1.6 MiB/s | 547.6 KiB | 00m00s + [5/9] rpm-build-libs-0:4.19.94-1.fc41.x 100% | 2.1 MiB/s | 99.1 KiB | 00m00s + [6/9] openssl-libs-1:3.2.2-7.fc41.x86_6 100% | 2.6 MiB/s | 2.3 MiB | 00m01s + [7/9] rpm-libs-0:4.19.94-1.fc41.x86_64 100% | 1.1 MiB/s | 309.5 KiB | 00m00s + [8/9] zlib-ng-compat-0:2.1.7-3.fc41.x86 100% | 1.0 MiB/s | 77.7 KiB | 00m00s + [9/9] systemd-libs-0:256.6-1.fc41.x86_6 100% | 2.6 MiB/s | 730.9 KiB | 00m00s + -------------------------------------------------------------------------------- + [9/9] Total 100% | 2.2 MiB/s | 5.4 MiB | 00m02s + Running transaction + [ 1/20] Verify package files 100% | 750.0 B/s | 9.0 B | 00m00s + [ 2/20] Prepare transaction 100% | 1.6 KiB/s | 18.0 B | 00m00s + [ 3/20] Upgrading libgcc-0:14.2.1-3.fc4 100% | 14.2 MiB/s | 276.3 KiB | 00m00s + >>> Running post-install scriptlet: libgcc-0:14.2.1-3.fc41.x86_64 + >>> Stop post-install scriptlet: libgcc-0:14.2.1-3.fc41.x86_64 + [ 4/20] Upgrading zlib-ng-compat-0:2.1. 100% | 32.9 MiB/s | 134.8 KiB | 00m00s + [ 5/20] Upgrading rpm-libs-0:4.19.94-1. 100% | 117.7 MiB/s | 723.4 KiB | 00m00s + [ 6/20] Upgrading libgomp-0:14.2.1-3.fc 100% | 170.8 MiB/s | 524.8 KiB | 00m00s + [ 7/20] Upgrading rpm-build-libs-0:4.19 100% | 15.6 MiB/s | 207.5 KiB | 00m00s + >>> Running pre-install scriptlet: rpm-0:4.19.94-1.fc41.x86_64 + >>> Stop pre-install scriptlet: rpm-0:4.19.94-1.fc41.x86_64 + [ 8/20] Upgrading rpm-0:4.19.94-1.fc41. 100% | 104.3 MiB/s | 2.5 MiB | 00m00s + [ 9/20] Upgrading openssl-libs-1:3.2.2- 100% | 190.9 MiB/s | 7.8 MiB | 00m00s + [10/20] Upgrading libstdc++-0:14.2.1-3. 100% | 145.6 MiB/s | 2.8 MiB | 00m00s + [11/20] Upgrading systemd-libs-0:256.6- 100% | 135.3 MiB/s | 2.0 MiB | 00m00s + [12/20] Erasing rpm-build-libs-0:4.19.9 100% | 1.0 KiB/s | 5.0 B | 00m00s + [13/20] Erasing libstdc++-0:14.2.1-1.fc 100% | 6.1 KiB/s | 31.0 B | 00m00s + [14/20] Erasing systemd-libs-0:256.5-1. 100% | 2.4 KiB/s | 20.0 B | 00m00s + [15/20] Erasing rpm-0:4.19.92-6.fc41.x8 100% | 26.7 KiB/s | 273.0 B | 00m00s + [16/20] Erasing rpm-libs-0:4.19.92-6.fc 100% | 2.4 KiB/s | 10.0 B | 00m00s + [17/20] Erasing openssl-libs-1:3.2.2-5. 100% | 9.5 KiB/s | 39.0 B | 00m00s + [18/20] Erasing zlib-ng-compat-0:2.1.7- 100% | 1.2 KiB/s | 5.0 B | 00m00s + [19/20] Erasing libgcc-0:14.2.1-1.fc41. 100% | 647.0 B/s | 11.0 B | 00m00s + >>> Running post-uninstall scriptlet: libgcc-0:14.2.1-1.fc41.x86_64 + >>> Stop post-uninstall scriptlet: libgcc-0:14.2.1-1.fc41.x86_64 + [20/20] Erasing libgomp-0:14.2.1-1.fc41 100% | 43.0 B/s | 9.0 B | 00m00s + >>> Running post-transaction scriptlet: rpm-0:4.19.94-1.fc41.x86_64 + >>> Stop post-transaction scriptlet: rpm-0:4.19.94-1.fc41.x86_64 + >>> Running trigger-install scriptlet: glibc-common-0:2.40-3.fc41.x86_64 + >>> Stop trigger-install scriptlet: glibc-common-0:2.40-3.fc41.x86_64 + >>> Running trigger-post-uninstall scriptlet: glibc-common-0:2.40-3.fc41.x86_64 + >>> Stop trigger-post-uninstall scriptlet: glibc-common-0:2.40-3.fc41.x86_64 + Complete! + Removed 24 files, 13 directories. 0 errors occurred. + ==> 8a9a360: IMAGE /tmp/velocity/build/fedora-41-8a9a360/8a9a360.sif (fedora@41) BUILT [0:00:21] + + ==> de9c02b: BUILD hello-world@1.0 ... + ==> de9c02b: COPYING FILES ... + FILE: /tmp/velocity/images/hello-world/files/hello_world.py -> /tmp/velocity/build/hello-world-1.0-de9c02b/hello_world.py + ==> de9c02b: GENERATING SCRIPT ... + SCRIPT: /tmp/velocity/build/hello-world-1.0-de9c02b/script + Bootstrap: localimage + From: /tmp/velocity/build/fedora-41-8a9a360/8a9a360.sif + + %files + hello_world.py /hello_world + + %post + dnf -y install python3 + chmod +x /hello_world + + %runscript + /hello_world + ==> de9c02b: BUILDING ... + #!/usr/bin/env bash + apptainer build --disable-cache /tmp/velocity/build/hello-world-1.0-de9c02b/de9c02b.sif /tmp/velocity/build/hello-world-1.0-de9c02b/script; + Updating and loading repositories: + Fedora 41 openh264 (From Cisco) - x86_ 100% | 11.1 KiB/s | 6.0 KiB | 00m01s + Fedora 41 - x86_64 100% | 11.7 MiB/s | 35.4 MiB | 00m03s + Fedora 41 - x86_64 - Updates 100% | 50.1 KiB/s | 31.9 KiB | 00m01s + Fedora 41 - x86_64 - Test Updates 100% | 1.8 MiB/s | 2.1 MiB | 00m01s + Repositories loaded. + Package Arch Version Repository Size + Installing: + python3 x86_64 3.13.0~rc2-1.fc41 fedora 31.8 KiB + Installing dependencies: + expat x86_64 2.6.3-1.fc41 updates-testing 291.5 KiB + libb2 x86_64 0.98.1-12.fc41 fedora 42.2 KiB + mpdecimal x86_64 2.5.1-16.fc41 fedora 204.9 KiB + python-pip-wheel noarch 24.2-1.fc41 fedora 1.2 MiB + python3-libs x86_64 3.13.0~rc2-1.fc41 fedora 40.3 MiB + Installing weak dependencies: + python-unversioned-command noarch 3.13.0~rc2-1.fc41 fedora 23.0 B + + Transaction Summary: + Installing: 7 packages + + Total size of inbound packages is 11 MiB. Need to download 11 MiB. + After this operation 42 MiB will be used (install 42 MiB, remove 0 B). + [1/7] libb2-0:0.98.1-12.fc41.x86_64 100% | 94.1 KiB/s | 25.7 KiB | 00m00s + [2/7] python3-0:3.13.0~rc2-1.fc41.x86_6 100% | 95.9 KiB/s | 27.4 KiB | 00m00s + [3/7] mpdecimal-0:2.5.1-16.fc41.x86_64 100% | 408.0 KiB/s | 89.0 KiB | 00m00s + [4/7] expat-0:2.6.3-1.fc41.x86_64 100% | 447.4 KiB/s | 114.1 KiB | 00m00s + [5/7] python-pip-wheel-0:24.2-1.fc41.no 100% | 2.5 MiB/s | 1.2 MiB | 00m00s + [6/7] python-unversioned-command-0:3.13 100% | 125.1 KiB/s | 10.5 KiB | 00m00s + [7/7] python3-libs-0:3.13.0~rc2-1.fc41. 100% | 8.1 MiB/s | 9.1 MiB | 00m01s + -------------------------------------------------------------------------------- + [7/7] Total 100% | 6.7 MiB/s | 10.6 MiB | 00m02s + Running transaction + [1/9] Verify package files 100% | 269.0 B/s | 7.0 B | 00m00s + [2/9] Prepare transaction 100% | 280.0 B/s | 7.0 B | 00m00s + [3/9] Installing expat-0:2.6.3-1.fc41.x 100% | 71.7 MiB/s | 293.6 KiB | 00m00s + [4/9] Installing python-pip-wheel-0:24. 100% | 310.4 MiB/s | 1.2 MiB | 00m00s + [5/9] Installing mpdecimal-0:2.5.1-16.f 100% | 50.3 MiB/s | 206.0 KiB | 00m00s + [6/9] Installing libb2-0:0.98.1-12.fc41 100% | 4.7 MiB/s | 43.3 KiB | 00m00s + [7/9] Installing python3-libs-0:3.13.0~ 100% | 139.9 MiB/s | 40.7 MiB | 00m00s + [8/9] Installing python3-0:3.13.0~rc2-1 100% | 10.9 MiB/s | 33.6 KiB | 00m00s + [9/9] Installing python-unversioned-com 100% | 1.8 KiB/s | 424.0 B | 00m00s + >>> Running trigger-install scriptlet: glibc-common-0:2.40-3.fc41.x86_64 + >>> Stop trigger-install scriptlet: glibc-common-0:2.40-3.fc41.x86_64 + Complete! + ==> de9c02b: IMAGE /tmp/velocity/build/hello-world-1.0-de9c02b/de9c02b.sif (hello-world@1.0) BUILT [0:00:14] + + ==> BUILT: /tmp/velocity/hello-world-1.0_fedora-41__x86_64-fedora.sif + Our hello-world image has been built! .. code-block:: bash - :emphasize-lines: 3 + :emphasize-lines: 7 - user@hostname:~$ podman image ls - REPOSITORY TAG IMAGE ID CREATED SIZE - localhost/hello-world__1.0__x86_64__fedora latest db958bad4f40 4 minutes ago 337 MB - docker.io/library/fedora 41 54d1373b70a2 5 weeks ago 180 MB + user@hostname:~$ ls + total 190972 + drwxr-xr-x 4 xjv users 4096 Sep 18 10:01 . + drwxrwxrwt 31 root root 131072 Sep 18 10:01 .. + drwxr-xr-x 4 xjv users 4096 Sep 18 10:01 build + -rwxr-xr-x 1 xjv users 66007040 Sep 18 09:42 fedora-38__x86_64-fedora.sif + -rwxr-xr-x 1 xjv users 129392640 Sep 18 10:01 hello-world-1.0_fedora-41__x86_64-fedora.sif + drwxr-xr-x 4 xjv users 4096 Sep 18 09:44 images + + +Now you can run the image! + +.. code-block:: bash + user@hostname:~$ apptainer run hello-world-1.0_fedora-41__x86_64-fedora.sif + Hello, World! diff --git a/docs/build/html/index.html b/docs/build/html/index.html index 8e7130d..e4e71f5 100644 --- a/docs/build/html/index.html +++ b/docs/build/html/index.html @@ -4,7 +4,7 @@ - Velocity Container Build System — Velocity documentation + Velocity Container Build Management — Velocity documentation @@ -66,7 +66,7 @@
  • - +
  • View page source
  • @@ -76,8 +76,10 @@
    -
    -

    Velocity Container Build System

    +
    +

    Velocity Container Build Management

    +

    Velocity is a tool to help with the maintenance of container build scripts on multiple systems, backends +(e.g podman or apptainer) and distros.

    • Getting Started
        @@ -90,8 +92,9 @@

        Velocity Container Build System
        • Technical Docs
        • diff --git a/docs/build/html/objects.inv b/docs/build/html/objects.inv index 2254dafed232b111a60ee312e89f97ca8ffd8fda..8827a599a0d7fc542d16e72ca155a95ca3f5ed83 100644 GIT binary patch delta 382 zcmV-^0fGL`1MLHlcz;1|gD?=h`xTa&D~)>XscNfKIYdQD6sf1mF&nUCY_wiz`F$~l zfD_u@z%#SsS?>zW>k$KLqZuq{_MT@P8gAc&(BJmEjU$eG6JF0&StfS!Lvg6bRchAF=c9~tU{!YoQ&kB z?uX54|6`iARCKDy`>Jj8BRoH#dqzk-7L0J1Pj3e8`w zoZI)~wJe_|k9Y6;&sLP0#jRadUYgF2hh54y7E3|Qfk0VY&1`=*m;^lSCW3kIMxYX> zKjt-BjSHK_doaJ@!iMno2Cz}^HOX!|8Y4mW2IP~@E^VRp^4L78B0=wY-D z6j7#HNa@D)1Vo#?sd@>Qybj^h0>7qNj;WegOB0SM`y4u%Q3| diff --git a/docs/build/html/reference/build.html b/docs/build/html/reference/build.html index bc6183e..320f4e6 100644 --- a/docs/build/html/reference/build.html +++ b/docs/build/html/reference/build.html @@ -22,7 +22,7 @@ - + @@ -50,8 +50,9 @@
        • Technical Docs
            +
          • Configuration
          • VTMP Format
          • -
          • Specifications.yaml
          • +
          • Specs.yaml
          • Build Process
            • Setup
            • Files
            • @@ -95,14 +96,12 @@

              Build Process

              Setup

              -

              After selecting images to build and ordering them. Velocity will start by assigning a random -id to each image to be built. It will also create a directory BSD (Build Sub Dir) named with that random id in -VELOCITY_BUILD_DIR.

              +

              After selecting images to build and ordering them Velocity will also create a directory BSD (Build Sub Dir) +named <name>-<version>-<hash> in build directory.

    Files

    -

    Next Velocity will look for a folder in the image definition directory with the same name as VELOCITY_SYSTEM. All -of the files in this directory will be copied to the BSD.

    +

    Next Velocity will copy any files that you specified.

    Parse Template

    @@ -119,15 +118,18 @@

    Build

    Build Directory Layout

    -

    At the end of a build VELOCITY_BUILD_DIR should look somthing like this. With the addition of any other files that +

    At the end of a build the build dir should look somthing like this. With the addition of any other files that got copied in the files step.

    .
    -├── jknqsnkc
    -│   ├── build
    -│   ├── log
    -│   └── script
    -└── nfhmhmsh
    +├── fedora-41-8a9a360
    +│ ├── 8a9a360.sif
    +│ ├── build
    +│ ├── log
    +│ └── script
    +└── hello-world-1.0-de9c02b
         ├── build
    +    ├── de9c02b.sif
    +    ├── hello_world.py
         ├── log
         └── script
     
    @@ -139,7 +141,7 @@

    Build Directory Layout


    diff --git a/docs/build/html/reference/config.html b/docs/build/html/reference/config.html new file mode 100644 index 0000000..7dfb61c --- /dev/null +++ b/docs/build/html/reference/config.html @@ -0,0 +1,191 @@ + + + + + + + Configuration — Velocity documentation + + + + + + + + + + + + + + + + + + + + +
    + + +
    + +
    +
    +
    + +
    +
    +
    +
    + +
    +

    Configuration

    +

    Configuration for Velocity comes from three places. Command line options, environment variables and the configuration file.

    +
    +

    Commandline Options

    +

    These take the highest level of precedence and can be viewed by using the --help option in the command line.

    +
    +
    +

    Variables

    +

    Variables are the second highest level of configuration.

    +
    +

    VELOCITY_IMAGE_PATH

    +

    This variable points to the directories containing the the image definitions.

    +
    +
    +

    VELOCITY_SYSTEM

    +

    This variable specifies what computer system you are building for (e.g. frontier).

    +
    +
    +

    VELOCITY_BACKEND

    +

    This variable specifies the container backend that should be used (e.g podman).

    +
    +
    +

    VELOCITY_DISTRO

    +

    This variable specifies the distro of the container images that will be built. This name is flexable and completely +up to the user. It is used purely for organizational purposes.

    +
    +
    +

    VELOCITY_BUILD_DIR

    +

    This variable specifies a scratch space for Velocity to preform builds in.

    +
    +
    +

    VELOCITY_CONFIG_DIR

    +

    This variable specifies where to look for the configuration file.

    +
    +
    +
    +

    Configuration File

    +

    The configuration file is the lowest level of configuration. By default Velocity looks for config.yaml in +~/.velocity unless VELOCITY_CONFIG_DIR is set. A number of configuration option for velocity can be set.

    +
    velocity:
    +  system: frontier
    +  backend: apptainer
    +  distro: ubuntu
    +  debug: INFO   # set the debug level
    +  image_path:   # a list of : seperated paths
    +  build_dir:    # path to a scratch space
    +
    +
    +

    Additionally you can set arguments and variables at a global level in the constraints section. As an example here +we are adding --disable-cache as an argument for every image build we do with apptainer.

    +
    constraints:
    +  arguments:
    +    - value: --disable-cache
    +      when: backend=apptainer
    +
    +
    +
    +
    + + +
    +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/docs/build/html/reference/index.html b/docs/build/html/reference/index.html index 2931e30..26ccfca 100644 --- a/docs/build/html/reference/index.html +++ b/docs/build/html/reference/index.html @@ -22,7 +22,7 @@ - + @@ -51,8 +51,9 @@

  • Technical Docs
  • @@ -86,15 +87,23 @@

    Technical Docs


    diff --git a/docs/build/html/reference/specifications.html b/docs/build/html/reference/specifications.html deleted file mode 100644 index 7fec271..0000000 --- a/docs/build/html/reference/specifications.html +++ /dev/null @@ -1,278 +0,0 @@ - - - - - - - Specifications.yaml — Velocity documentation - - - - - - - - - - - - - - - - - - - - -
    - - -
    - -
    -
    -
    - -
    -
    -
    -
    - -
    -

    Specifications.yaml

    -
    -

    Basic Layout

    -

    The specifications.yaml file defines image availability, dependencies and a number of other available -options for an image. Velocity doesn’t care about any extraneous data you may have in it. All that Velocity looks -for is a base level section called build_specifications. Any other sections that you may add for your own purposes -are ignored by Velocity. Under build_specifications there should be a tree defining under what conditions the image -can be built in the order system > backend > distro. For example if I am creating an centos based image for use on -frontier which only has Apptainer on it then I should have the following structure in my specifications.yaml file.

    -
    build_specifications:
    -  frontier:
    -    apptainer:
    -      ubuntu:
    -        # config options
    -
    -
    -

    If I wanted it to be available on summit as well which has Apptainer and Podman I would add the following.

    -
    build_specifications:
    -  frontier:
    -    apptainer:
    -      centos:
    -        # config options
    -  summit:
    -    apptainer:
    -      centos:
    -        # config options
    -    podman:
    -      centos:
    -        # config options
    -
    -
    -

    While system and backend are fairly self explanatory the distro is a little more obscure. Velocity does not do -any checks to make sure that the image being built is actually of the right distro. All that the distro tells Velocity to do is -look in the templates folder of an image for a template called <distro>.vtmp (e.g. centos.vtmp). If a template with -the right name cannot be found Velocity will error.

    -
    -
    image structure
    -
    .
    -└── <image name>
    -    └── <image version>
    -        ├── specifications.yaml
    -        └── templates
    -            └── <distro>.vtmp
    -
    -
    -
    -
    -
    -

    Config Options

    -

    Under each system > backend > distro section there are a variaty of config options available.

    -
    -

    dependencies

    -

    The dependencies section is a list of images that must be built before the current one can be.

    -
    build_specifications:
    -  frontier:
    -    apptainer:
    -      ubuntu:
    -        dependencies:
    -          - dep_one
    -          - dep_two
    -            # ...
    -
    -
    -

    Some times you may want a specific version of a dependency in which case Velocity provides several ways to specify dependency -relationships. Versions are compared by splitting the version string up based on periods and then comparing the sections -left to right alphanumerically.

    -
    build_specifications:
    -  frontier:
    -    apptainer:
    -      ubuntu:
    -        dependencies:
    -          - dep_one@=11.5       # equal to 11.5
    -          - dep_two@_11.5       # less than or equal to 11.5
    -          - dep_three@^11.5     # greater than or equal to 11.5
    -          - dep_four@11.2%11.8  # inbetween 11.2 and 11.8 (inclusive)
    -            # ...
    -
    -
    -
    -
    -

    prolog

    -

    There are certain occasions when some commands need to be run on the host machine before an image is built. These -commands can be placed in a bash script in a directory which matches the system name. All that you need to do in the -specifications.yaml file is put the script name.

    -
    build_specifications:
    -  frontier:
    -    apptainer:
    -      ubuntu:
    -        dependencies:
    -          - dep_one
    -          - dep_two
    -            # ...
    -        prolog: example_prolog.sh
    -
    -
    -
    -
    VELOCITY_IMAGE_DIR
    -
    .
    -├── <image name>
    -    └── <image version>
    -        ├── <system>
    -            └── example_prolog.sh
    -        ├── specifications.yaml
    -        └── templates
    -            └── <distro>.vtmp
    -
    -
    -
    -
    -
    -

    arguments

    -

    Any arguments specified here will be added to the build command.

    -
    build_specifications:
    -  frontier:
    -    apptainer:
    -      ubuntu:
    -        dependencies:
    -          - dep_one
    -          - dep_two
    -            # ...
    -        prolog: example_prolog.sh
    -        arguments:
    -          - '--build-arg EXAMPLE=8046'
    -
    -
    -

    This will result in apptainer build –build-arg EXAMPLE=8046 … when the image is built.

    -
    -
    -

    variables

    -

    Here you can define variables for the VTMP file to use when it is parsed.

    -
    build_specifications:
    -  frontier:
    -    apptainer:
    -      ubuntu:
    -        dependencies:
    -          - dep_one
    -          - dep_two
    -            # ...
    -        prolog: example_prolog.sh
    -        arguments:
    -          - '--build-arg EXAMPLE=8046'
    -        variables:
    -          - arch: x86_64
    -          - url: 'https://example.com'
    -
    -
    -
    -
    -
    - - -
    -
    - -
    -
    -
    -
    - - - - \ No newline at end of file diff --git a/docs/build/html/reference/specs.html b/docs/build/html/reference/specs.html new file mode 100644 index 0000000..62f032e --- /dev/null +++ b/docs/build/html/reference/specs.html @@ -0,0 +1,234 @@ + + + + + + + Specs.yaml — Velocity documentation + + + + + + + + + + + + + + + + + + + + +
    + + +
    + +
    +
    +
    + +
    +
    +
    +
    + +
    +

    Specs.yaml

    +
    +

    About

    +

    The specs.yaml file defines image availability, dependencies and a number of other available +options for an image.

    +
    +
    +

    Config Options

    +
    +

    versions

    +

    The versions section defines a list of versions and their availability.

    +
    versions:
    +  - spec: 45
    +    when: distro=ubuntu
    +  - spec:
    +      - 46
    +      - 47
    +    when: backend=podman
    +
    +
    +
    +
    +

    dependencies

    +

    The dependencies section is used to specify prerequist images.

    +
    dependencies:
    +  - spec: ubuntu
    +    when: distro=ubuntu
    +  - spec:
    +      - "gcc@12:"
    +      - pytorch
    +    when: myapp@:2.3
    +
    +
    +
    +
    +

    templates

    +

    By default velocity looks for a template called default.vtmp, but using the templates section you can specify an +alternate template.

    +
    templates:
    +  - name: second_tmp
    +    when: "myapp@2:"
    +
    +
    +
    +
    +

    arguments

    +

    Use the argument section to add build time arguments to an image.

    +
    arguments:
    +  - value: --disable-cache
    +    when: backend=apptainer
    +
    +
    +
    +
    +

    variables

    +

    Use the variables section to set template variables.

    +
    variables:
    +  - name: test_var
    +    value: "Test value."
    +
    +
    +
    +
    +

    files

    +

    Use the files section to specify files or directories in the files folder that you want copied to the build directory.

    +
    files:
    +  - name: config.json
    +
    +
    +
    +
    +

    prologs

    +

    Use the prologs section to bash commands that you want to run before the build.

    +
    prologs:
    +  - script: |
    +      git clone ...
    +    when: system=summit
    +
    +
    +
    +
    +
    +

    Using when

    +

    A few notes about using when to filter config options. The when option can be used to filter configs as shown +above by system, backend, distro and dependencies. The only exception to this is the versions section which cannot +be filtered by dependencies. List each item that you want to filter by separated by a space e.g. gcc@12.3 system=frontier. +Additionally you can specify the scope of a when by specifying the scope. The default scope is image which means +that the when statement is evaluated on the current image. So say you want to apply a config in the gcc specs.yaml file +to every gcc version greater than 10.3.0, you would use when: ^ubuntu:. Alternatively you can set the scope to build +when you want the when statement evaluated on the current build.

    +
    variable:
    +  - name: TEST
    +    value: "1 2 3 4"
    +    when: ubuntu
    +    scope: build
    +
    +
    +

    This can be read as “If the ubuntu image is in the current set of images to be built then add the TEST variable.”

    +
    +
    + + +
    +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/docs/build/html/reference/vtmp.html b/docs/build/html/reference/vtmp.html index 8e69f00..1ac30e1 100644 --- a/docs/build/html/reference/vtmp.html +++ b/docs/build/html/reference/vtmp.html @@ -22,8 +22,8 @@ - - + + @@ -51,12 +51,14 @@
  • Technical Docs
  • @@ -102,74 +103,92 @@

    VTMP Format

    VTMP files are used by Velocity to generate the appropriate script for a particular backend. -While certain features are supported across all backends, others are exclusive to one backend and many are interpreted -quite differently between the different backends. The goal of the VTMP files are to provide a unified API for +The goal of the VTMP files are to provide a unified API for container images across multiple backends but they are not feature exhaustive. VTMP files are split up into a number of sections.

    Example VTMP
    -
    @from
    -    # can only take one of these
    +
    >>> this is a comment
    +@from
    +    >>> can only take one of these examples
         docker.io/ubuntu:20.04
    -    /example.sif # only works for apptainer backend
    -    %(__base__)
    +    /example.sif    >>> only works for apptainer backend
    +    {{ __base__ }}  >>> build on the previous image
     
    -@pre # same as post but placed right after @from
    -    # anything here is taken literally
    -    # denote the start of a line with |
    -    |echo 'Without tab'
    +@pre
    +    >>> anything here is taken literally
    +    >>> optionally denote the start of a line with | (helpful for white space)
    +    echo 'Without tab'
         |    echo 'With tab'
     
    -@arg
    -    # define arg names
    -    example_arg # reference this argument throughout the script with @(<arg>)
    -
     @copy
    -    # list files to copy, format <src> <dest>
    +    >>> list files to copy, format <src> <dest>
         /external/file /internal/file
     
     @run
    -    # list of commands to run in container
    +    >>> list of commands to run in container
         echo 'Hello world'
    -    ?podman echo 'podman'
    -    ?apptainer echo 'apptainer'
    -    echo %(template_variable)
    -    echo @(example_arg) # see @arg
    +    ?? distro=podman |> echo 'podman' ??    >>> conditional
    +    ?? distro=apptainer |> echo 'apptainer' ??
    +    echo {{ template_variable }}
    +    echo @@ example_arg @@  >>> define an argument
    +    !envar TEST_VAR I want this here and in the @env section    >>> the !envar directive
     
     @env
    -    # set env variables
    +    >>> set env variables
         PATH /something:$PATH
     
     @label
    -    # set container labels
    -    velocity.image.%(__name__)__%(__tag__) %(__hash__)
    -
    +    >>> set container labels
    +    IMAGE_NAME {{ __name__ }}
     
     @entry
    -    # set entrypoint or %runscript
    -    # only one line allowed
    +    >>> set  ENTRYPOINT or %runscript
    +    >>> only one line allowed
    +
         /bin/bash
     
    -@post # same as pre but placed after everything else
    +@post >>> same as pre but placed after everything else
     

    Variables

    The first thing that is done to a VTMP file is the substitution of any variables found in the script. Variables are indicated -by %(<variable>). In Velocity variables can be defined by the user in the specifications.yaml file, but Velocity also +by {{ <variable> }}. In Velocity variables can be defined by the user in the specs.yaml file, but Velocity also provides a set of build time variables.

    __backend__
     __distro__
    -__hash__        # template hash
    -__base__        # previous image to build on
    -__name__        # current image name e.g. cuda
    +__base__        >>> previous image to build on
    +__name__        >>> current image name e.g. cuda
     __system__
    -__tag__         # image "version" e.g. 11.7.1
    +__version__     >>> image "version" e.g. 11.7.1
    +__version_major__
    +__version_minor__
    +__version_patch__
    +__version_suffix__
     __timestamp__
    +>>> the versions of all images in a build are also available in the following format
    +__<name>__version__
    +__<name>__version_major__
    +__<name>__version_minor__
    +__<name>__version_patch__
    +__<name>__version_suffix__
     
    +
    +

    Arguments

    +

    You can also pass in arguments at build time using the form @@ <name> @@, but don’t forget to add the +argument in specs.yaml so that is gets added to the build command or the build will fail.

    +
    +
    +

    Conditionals

    +

    If there is a line in a template that you only want under certain conditions use ?? <condition> |> <text> ??. +The whole conditional will be replaced with the <text> section if the condition is true; otherwise, it will substitute +an empty string. The conditionals can include the following components, system=<value>, backend=<value>, +distro=<value> and a dependency in the form ^<value>.

    +

    Sections

    @@ -185,12 +204,6 @@

    @from are where they are placed in the script the @pre section is placed at the beginning of the script right after the @from section.

    -
    -

    @arg

    -

    The @arg section specifies build time arguments that are passed to the backend. If these arguments are not passed to -the backend at build time it will fail. Once an arg has been defined in the @arg section it can be referenced -throughout the script with @(<argument>).

    -

    @copy

    The @copy section takes a list of files/dir to be copied in the <src> <dest> format.

    @@ -198,12 +211,14 @@

    @copy

    @run

    The @run section takes a list of commands to be run in the container. These commands should be written as if they all -occur one after the other in the same shell.

    +occur one after the other in the same shell. One added feature to the @run section is the !envar directive. This +directive will add the following variable definition to the @env section as well as to the current @run section. Use +the format !envar <name> <value>.

    @env

    The @env section sets environment variables. These variables will only be available when the container is run or in -the next build so if there is a variable that is needed in the run section you must declare it there as well.

    +the next build so if there is a variable that is needed in the run section use the !envar directive in the @run section.

    @label

    @@ -220,19 +235,14 @@

    @post

    See @pre. The @post section is placed at the end of the script after all other sections.

    -
    -

    Conditionals

    -

    It is handy somtimes to be able to limit certain commands to a backend(s) this can be done by placing a ?<backend> -at the beginning of the line in question.

    -

    diff --git a/docs/build/html/searchindex.js b/docs/build/html/searchindex.js index 3de7bb0..61a2ffc 100644 --- a/docs/build/html/searchindex.js +++ b/docs/build/html/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles": {"@arg": [[4, "arg"]], "@copy": [[4, "copy"]], "@entry": [[4, "entry"]], "@env": [[4, "env"]], "@from": [[4, "from"]], "@label": [[4, "label"]], "@post": [[4, "post"]], "@pre": [[4, "pre"]], "@run": [[4, "run"]], "Adding Different Versions": [[7, "adding-different-versions"]], "Base Image": [[7, "base-image"]], "Basic Layout": [[3, "basic-layout"]], "Basic Usage": [[5, "basic-usage"]], "Build": [[1, "build"]], "Build Directory Layout": [[1, "build-directory-layout"]], "Build Process": [[1, null]], "Concept": [[5, "concept"]], "Conditionals": [[4, "conditionals"]], "Config Options": [[3, "config-options"]], "Configuration": [[5, "configuration"]], "Create Build Script": [[1, "create-build-script"]], "Creating Your First Image": [[7, null]], "Files": [[1, "files"]], "Getting Started": [[6, null]], "Hello World!": [[7, "hello-world"]], "How it Works": [[5, "how-it-works"]], "Installation": [[5, "installation"]], "Layout": [[5, "layout"]], "Overview": [[5, null]], "Parse Template": [[1, "parse-template"]], "Prerequisites": [[7, "prerequisites"]], "Sections": [[4, "sections"]], "Setup": [[1, "setup"]], "Specifications.yaml": [[3, null]], "Specifying Version": [[7, "specifying-version"]], "Technical Docs": [[2, null]], "VELOCITY_BACKEND": [[5, "velocity-backend"]], "VELOCITY_BUILD_DIR": [[5, "velocity-build-dir"]], "VELOCITY_DISTRO": [[5, "velocity-distro"]], "VELOCITY_IMAGE_DIR": [[5, "velocity-image-dir"]], "VELOCITY_SYSTEM": [[5, "velocity-system"]], "VTMP Format": [[4, null]], "Variables": [[4, "variables"]], "Velocity Container Build System": [[0, null]], "arguments": [[3, "arguments"]], "avail": [[5, "avail"]], "build": [[5, "build"]], "dependencies": [[3, "dependencies"]], "edit": [[5, "edit"]], "prolog": [[3, "prolog"]], "spec": [[5, "spec"]], "variables": [[3, "variables"]]}, "docnames": ["index", "reference/build", "reference/index", "reference/specifications", "reference/vtmp", "starting/basic", "starting/index", "starting/tutorial"], "envversion": {"sphinx": 62, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst", "reference/build.rst", "reference/index.rst", "reference/specifications.rst", "reference/vtmp.rst", "starting/basic.rst", "starting/index.rst", "starting/tutorial.rst"], "indexentries": {}, "objects": {}, "objnames": {}, "objtypes": {}, "terms": {"": [4, 5, 7], "0": [5, 7], "00": [5, 7], "01": [5, 7], "02": 7, "03": 7, "04": [4, 7], "06": 7, "07": 7, "09": 7, "0__x86_64__fedora": 7, "1": [4, 5, 7], "10": [5, 7], "100": 7, "101": 7, "102": 7, "103": 7, "104": 7, "105": 7, "106": 7, "107": 7, "108": 7, "109": 7, "11": [3, 4, 5, 7], "110": 7, "111": 7, "112": 7, "113": 7, "114": 7, "115": 7, "116": 7, "117": 7, "118": 7, "119": 7, "12": 7, "120": 7, "121": 7, "122": 7, "123": 7, "124": 7, "125": 7, "126": 7, "127": 7, "128": 7, "129": 7, "13": 7, "130": 7, "131": 7, "136": 7, "14": 7, "140": 7, "146": 7, "149": 7, "15": 7, "152": 7, "153": 7, "155": 7, "158": 7, "159": 7, "16": 7, "165": 7, "168": 7, "17": 7, "17c3457c281": 7, "17c3457c281309909691b183b77323eb56390792a787e4a319b494d40868c907": 7, "18": 7, "180": 7, "181": 7, "187": 7, "19": [5, 7], "190": 7, "191": 7, "196": 7, "199": 7, "1ad7926d7e542fa521fe4a2eca54aa73caea82958b1f07adf04728b5762063ac": 7, "1b080f458c34ae2759d1eb3e8464d3d508e3bcb981a476f70709b0f20b6218bb": 7, "2": [3, 5, 7], "20": [4, 7], "20230520": 7, "2024": 7, "20240201": 7, "20240320": 7, "2024a": 7, "203": 7, "205": 7, "208": 7, "209": 7, "21": 7, "212": 7, "21211d9de40": 7, "21211d9de40aa6c0cb6b625e6f4fed265c88f9a4be09c4edcd93a78360580ecf": 7, "215": 7, "218": 7, "219": 7, "22": [5, 7], "224": 7, "23": [5, 7], "232": 7, "233": 7, "24": 7, "242": 7, "249": 7, "25": 7, "253": 7, "255": 7, "258": 7, "26": 7, "263": 7, "27": 7, "270": 7, "271": 7, "28": 7, "286": 7, "29": 7, "3": [5, 7], "30": 7, "301": 7, "309": 7, "31": 7, "315": 7, "317": 7, "32": 7, "323": 7, "324": 7, "33": 7, "331": 7, "336": 7, "337": 7, "34": [5, 7], "344": 7, "345": 7, "348": 7, "35": 7, "357": 7, "36": 7, "37": 7, "38": 7, "386": 7, "39": 7, "391": 7, "393": 7, "4": [5, 7], "40": [5, 7], "402": 7, "404": 7, "406": 7, "41": 7, "42": 7, "423": 7, "425": 7, "426": 7, "43": 7, "43089713ea9": 7, "44": 7, "45": 7, "458": 7, "46": 7, "47": 7, "48": 7, "49": 7, "49321c240b5": 7, "49321c240b522a0d66cd30b62addc99e87b5861e2e6c27f6d0136968c73be5aa": 7, "5": [3, 5, 7], "50": 7, "501": 7, "503": 7, "504": 7, "507": 7, "51": 7, "52": 7, "53": 7, "530": 7, "534": 7, "537": 7, "54": 7, "545": 7, "54d1373b70a2": 7, "55": 7, "554": 7, "56": 7, "57": 7, "579": 7, "58": 7, "580": 7, "59": 7, "594": 7, "6": [5, 7], "60": 7, "606": 7, "61": 7, "611": 7, "613": 7, "62": 7, "623": 7, "63": 7, "637": 7, "64": 7, "640": 7, "642": 7, "649": 7, "65": 7, "66": 7, "668": 7, "67": 7, "678": 7, "68": 7, "681": 7, "686": 7, "687": 7, "69": 7, "697": 7, "7": [4, 5, 7], "70": 7, "706": 7, "708": 7, "71": 7, "716": 7, "72": 7, "73": 7, "739": 7, "74": 7, "744": 7, "75": 7, "76": 7, "767": 7, "77": 7, "78": 7, "783": 7, "787": 7, "787eba03a2": 7, "79": 7, "8": [3, 5, 7], "80": 7, "8046": 3, "807": 7, "808": 7, "81": 7, "82": 7, "83": 7, "834": 7, "837": 7, "84": 7, "847": 7, "85": 7, "859": 7, "86": 7, "87": 7, "870": 7, "872": 7, "88": 7, "881": 7, "89": 7, "8__x86_64__cento": 5, "9": 7, "90": 7, "9000": 7, "901": 7, "91": 7, "919": 7, "92": 7, "93": 7, "94": 7, "95": 7, "96": [5, 7], "97": 7, "98": 7, "980": 7, "99": 7, "A": 4, "At": 1, "By": 5, "For": [3, 5, 7], "If": [3, 4, 5, 7], "In": [4, 7], "It": [1, 4], "One": 4, "That": 7, "The": [3, 4, 5], "There": [3, 5], "These": [3, 4], "To": 7, "With": [1, 4], "_11": 3, "__": [4, 7], "__backend__": [4, 7], "__base__": [4, 7], "__distro__": [4, 7], "__hash__": [4, 7], "__name__": [4, 7], "__system__": [4, 7], "__tag__": [4, 7], "__timestamp__": 4, "abil": 4, "abl": [4, 5], "about": 3, "across": 4, "actual": 3, "ad": [3, 4, 6], "add": [3, 4, 5, 7], "addit": 1, "after": [1, 4], "ago": 7, "ahead": 7, "all": [1, 3, 4, 5], "allow": 4, "alphanumer": [3, 7], "also": [1, 4, 5, 7], "am": 3, "an": [1, 3, 4, 5, 7], "ani": [1, 3, 4], "anyth": 4, "api": 4, "append": 1, "appropri": 4, "apptain": [3, 4, 5, 7], "ar": [3, 4, 5, 7], "arch": [3, 7], "arg": 3, "argument": [4, 5], "assign": 1, "audit": 7, "authselect": 7, "avail": [3, 4, 7], "b": [5, 7], "backend": [3, 4, 5, 7], "base": [3, 4, 5, 6], "bash": [3, 4, 7], "basic": [2, 6], "bdvdbcor": 7, "been": [4, 7], "befor": [3, 7], "begin": 4, "being": 3, "between": 4, "bin": [4, 7], "both": [4, 5], "bsd": 1, "build": [2, 3, 4, 7], "build_specif": [3, 7], "built": [1, 3, 5, 7], "call": [1, 3, 5, 7], "can": [3, 4, 5, 7], "cannot": 3, "care": 3, "case": 3, "cc": 5, "cento": [3, 5], "centos__stream8__x86_64__cento": 5, "certain": [3, 4], "chain": 5, "chang": 7, "charact": 4, "check": [3, 7], "choos": 5, "cisco": 7, "cleanup": 7, "clone": [5, 7], "code": 7, "colorama": 5, "com": 3, "command": [1, 3, 4, 5], "commit": 7, "common": 7, "compar": [3, 7], "complet": 7, "complic": 7, "compon": 5, "comput": 5, "concept": 6, "conda": 5, "condit": [2, 3], "config": [2, 7], "configur": 6, "contain": [4, 5, 7], "content": 5, "convert": 4, "copi": [1, 7], "core": 7, "correct": 1, "cp": 7, "creat": [0, 2, 3, 5, 6], "crypto": 7, "cuda": [4, 5], "cudnn": 5, "curl": 7, "current": [3, 4], "custom": 5, "d": [5, 7], "d58637575df": 7, "data": [3, 7], "db958bad4f40": 7, "declar": 4, "default": [5, 7], "defin": [3, 4, 5], "definit": [1, 5], "denot": 4, "dep_four": 3, "dep_on": 3, "dep_thre": 3, "dep_two": 3, "depend": [5, 7], "describ": 1, "desir": 5, "dest": 4, "develop": 7, "development": 7, "differ": [4, 6], "dir": [1, 4, 7], "directori": [2, 3, 5, 7], "distro": [3, 5, 7], "dnf": 7, "do": [3, 7], "doc": [0, 5], "docker": [4, 7], "document": 5, "doe": 3, "doesn": 3, "done": 4, "download": 7, "e": [3, 4, 5, 7], "each": [1, 3, 5, 7], "echo": 4, "editor": 5, "edu": 7, "elfutil": 7, "els": 4, "empti": 7, "enabl": 4, "end": [1, 4], "entri": 7, "entrypoint": [4, 7], "env": [5, 7], "environ": [4, 5, 7], "equal": 3, "error": [3, 7], "everyth": [4, 7], "exampl": [3, 4, 7], "example_arg": 4, "example_prolog": 3, "exclus": 4, "exhaust": 4, "exit": 5, "expat": 7, "expir": 7, "explanatori": 3, "extern": 4, "extran": 3, "f": 7, "fa1896e8689": 7, "fail": 4, "fairli": [3, 5], "fakeroot": 7, "fc": 7, "fc3": 7, "fc38": 7, "fc4": 7, "fc40": 7, "fc41": 7, "featur": 4, "fedora": [5, 7], "fedora__38": 7, "fedora__38__x86_64__fedora": 7, "fedora__41": 7, "ff9fa85cf102560cf3fe2014c3c758fbb3809247537abbeab2c4b67c62dda164": 7, "file": [2, 3, 4, 5, 7], "filesystem": 7, "final": [1, 5], "first": [0, 4, 5, 6], "five": 5, "flag": 7, "folder": [1, 3, 5, 7], "follow": [3, 5, 7], "format": [0, 2], "found": [3, 4], "fr": 7, "from": [5, 7], "frontier": [3, 5], "g": [3, 4, 5, 7], "gcc": 5, "gener": [1, 4, 5, 7], "get": [0, 7], "git": [5, 7], "git58e3d95": 7, "git9f501f3": 7, "gitlab": 5, "give": 7, "given": 5, "glib2": 7, "glibc": 7, "gmp": 7, "gnupg2": 7, "gnutl": 7, "go": 7, "goal": 4, "goe": 1, "got": 1, "gov": 5, "great": 7, "greater": 3, "h": 5, "ha": [3, 4, 5, 7], "handi": 4, "hash": 4, "have": [3, 4, 7], "hawkei": 7, "hello": [4, 6], "hello_world": 7, "help": 5, "here": [3, 4], "hmmer": 5, "hold": 5, "home": 7, "host": 3, "hostnam": [5, 7], "how": 6, "html": 5, "http": [3, 5, 7], "i": [3, 4, 5, 7], "iarfnxer": 5, "id": [1, 7], "ident": 7, "ignor": 3, "imag": [0, 1, 3, 4, 5, 6], "inbetween": 3, "includ": 4, "inclus": 3, "indent": 5, "index": 5, "indic": 4, "info": 5, "inform": 5, "insert": 4, "instal": 6, "intern": 4, "interpret": 4, "io": [4, 7], "ip": 7, "jknqsnkc": 1, "k": 7, "kalign": 5, "kb": 7, "kei": 4, "keyutil": 7, "l": 7, "label": 7, "langpack": 7, "last": 7, "later": 7, "latest": [5, 7], "layout": [2, 6], "left": [3, 7], "less": 3, "let": 7, "level": 3, "lib": [5, 7], "libassuan": 7, "libblkid": 7, "libcurl": 7, "libdnf": 7, "libeconf": 7, "libelf": 7, "libffi": 7, "libgcc": 7, "libgcrypt": 7, "libgomp": 7, "libgpg": 7, "libksba": 7, "libmodulemd": 7, "libmount": 7, "libnghttp2": 7, "librari": 7, "librepo": 7, "libreport": 7, "libsmartcol": 7, "libsolv": 7, "libssh": 7, "libstdc": 7, "libtirpc": 7, "libunistr": 7, "libuuid": 7, "libxml2": 7, "libzstd": 7, "like": [1, 5, 7], "limit": 4, "line": 4, "linux": 7, "list": [3, 4, 5], "liter": 4, "littl": [3, 7], "localhost": [5, 7], "locat": 5, "log": 1, "look": [1, 3, 7], "lookup": 5, "lxogjapp": 5, "m": 7, "machin": 3, "mai": 3, "mainten": 5, "make": [3, 7], "mani": 4, "mar": 7, "match": 3, "math": 7, "mb": 7, "md": 5, "messag": 5, "metadata": 7, "miniforge3": 5, "minim": 7, "minut": 7, "mirror": 7, "modular": 7, "more": [3, 5, 7], "move": 7, "mpi": 5, "multipl": [4, 5], "must": [3, 4], "my": 3, "name": [1, 3, 4, 7], "ncurs": 7, "need": [3, 4, 5, 7], "networkx": 5, "new": 7, "next": [1, 4, 5, 7], "nfhmhmsh": 1, "noa": 7, "noarc": 7, "noarch": 7, "notic": 7, "now": [5, 7], "npth": 7, "number": [3, 4], "o": 7, "obscur": 3, "occas": 3, "occur": 4, "olcf": 5, "onc": 4, "one": [3, 4, 5], "onli": [3, 4], "openh264": 7, "openssl": 7, "option": [2, 5, 7], "order": [1, 3, 5, 7], "ornl": 5, "other": [1, 3, 4], "our": 7, "output": [1, 7], "overview": [0, 6], "own": 3, "p": 7, "packag": [5, 7], "page": 1, "pars": [2, 3], "particular": 4, "pass": 4, "path": [4, 7], "pbxpudvh": 5, "pcre2": 7, "period": [3, 7], "piec": 1, "pip": 7, "place": [1, 3, 4], "podman": [3, 4, 5, 7], "point": [1, 5], "polici": 7, "posit": 5, "preform": 5, "prepar": 7, "prerequisit": 6, "previou": 4, "princeton": 7, "print": [5, 7], "process": [0, 2], "program": 5, "prolog": 1, "provid": [3, 4, 5], "pub": 7, "pull": [4, 7], "purpos": [3, 5], "put": 3, "py": 7, "python": [5, 7], "python3": 7, "python__3": 5, "pytorch": 5, "pyyaml": 5, "question": 4, "quit": 4, "random": 1, "rawhid": 7, "rc1": 7, "rc2": 7, "rc3": 7, "readm": 5, "refer": 4, "referenc": 4, "registri": 4, "relat": 4, "relationship": 3, "releas": 7, "repo": [5, 7], "repositori": [5, 7], "resolv": 7, "result": [1, 3], "rf": 7, "right": [3, 4, 7], "rp": 7, "rpm": 7, "rtd": 5, "run": [1, 3, 5, 7], "runscript": 4, "same": [1, 4], "saue": 5, "scope": 7, "scratch": 5, "script": [2, 3, 4, 5, 7], "scriptlet": 7, "section": [2, 3, 7], "see": [4, 5, 7], "select": 1, "self": 3, "separ": 4, "set": [4, 5, 7], "setup": [2, 5, 7], "sever": [3, 5, 7], "sh": [3, 5, 7], "shadow": 7, "shell": 4, "should": [1, 3, 4, 5, 7], "show": 5, "sif": 4, "simpl": [5, 7], "size": 7, "so": [4, 5, 7], "softwar": 5, "some": [3, 7], "someth": 4, "somth": 1, "somtim": 4, "sourc": [5, 7], "space": [4, 5], "specif": [0, 2, 4, 5, 7], "specifi": [1, 3, 4, 5, 6], "spectrum": 5, "sphinx": 5, "split": [3, 4, 7], "sqlite": 7, "src": 4, "start": [0, 1, 4, 7], "statu": 7, "step": [1, 7], "stream8": 5, "string": [3, 7], "structur": [3, 5], "sub": 1, "substitut": 4, "succeed": 7, "successfulli": 7, "summari": 7, "summit": [3, 5], "sunflyhd": 5, "support": 4, "sure": 3, "syntax": 7, "system": [3, 5, 7], "systemd": 7, "t": [3, 7], "tab": 4, "tag": [5, 7], "take": [4, 5], "taken": 4, "target": 5, "technic": 0, "tell": 3, "templat": [2, 3, 4, 7], "template_vari": 4, "test": 7, "text": 4, "than": 3, "thei": [4, 5], "them": 1, "theme": 5, "thi": [1, 3, 4, 5, 7], "thing": 4, "through": 1, "throughout": 4, "time": [3, 4, 7], "tmp": 7, "togeth": 1, "tool": 5, "top": 4, "total": 7, "transact": 7, "tree": [3, 5], "tutori": 7, "tzdata": 7, "ubuntu": [3, 4], "under": 3, "underneath": 5, "unifi": 4, "up": [3, 4, 5, 7], "updat": 7, "upgrad": 7, "url": 3, "us": [3, 4, 5, 7], "usag": 6, "user": [4, 5, 7], "usr": 7, "util": 7, "v": [5, 7], "valu": [4, 5], "variabl": [2, 5, 7], "variati": 3, "varieti": 5, "veloc": [1, 3, 4, 5, 7], "velocity_backend": 7, "velocity_build_dir": [1, 7], "velocity_distro": 7, "velocity_image_dir": [3, 7], "velocity_root": 7, "velocity_system": [1, 7], "verifi": 7, "version": [3, 4, 5, 6], "vim": 7, "visit": 5, "vtmp": [0, 2, 3, 5, 7], "wa": 1, "wai": 3, "want": [3, 7], "we": 7, "wed": 7, "week": 7, "well": [3, 4], "what": [3, 5], "whatev": 7, "wheel": 7, "when": [1, 3, 4, 7], "where": 4, "which": [3, 5, 7], "while": [3, 4], "white": 4, "wish": [5, 7], "without": 4, "wmrxxohi": 7, "work": [4, 6], "world": [4, 6], "world__1": 7, "would": 3, "written": 4, "x86": 7, "x86_64": [3, 5, 7], "xjv": 7, "xuoykrdt": 7, "xz": 7, "y": 7, "yama": 7, "yaml": [0, 2, 4, 5, 7], "yftozouc": 7, "you": [3, 4, 5, 7], "your": [0, 3, 6], "yum": 7}, "titles": ["Velocity Container Build System", "Build Process", "Technical Docs", "Specifications.yaml", "VTMP Format", "Overview", "Getting Started", "Creating Your First Image"], "titleterms": {"ad": 7, "arg": 4, "argument": 3, "avail": 5, "base": 7, "basic": [3, 5], "build": [0, 1, 5], "concept": 5, "condit": 4, "config": 3, "configur": 5, "contain": 0, "copi": 4, "creat": [1, 7], "depend": 3, "differ": 7, "directori": 1, "doc": 2, "edit": 5, "entri": 4, "env": 4, "file": 1, "first": 7, "format": 4, "from": 4, "get": 6, "hello": 7, "how": 5, "imag": 7, "instal": 5, "label": 4, "layout": [1, 3, 5], "option": 3, "overview": 5, "pars": 1, "post": 4, "pre": 4, "prerequisit": 7, "process": 1, "prolog": 3, "run": 4, "script": 1, "section": 4, "setup": 1, "spec": 5, "specif": 3, "specifi": 7, "start": 6, "system": 0, "technic": 2, "templat": 1, "usag": 5, "variabl": [3, 4], "veloc": 0, "velocity_backend": 5, "velocity_build_dir": 5, "velocity_distro": 5, "velocity_image_dir": 5, "velocity_system": 5, "version": 7, "vtmp": 4, "work": 5, "world": 7, "yaml": 3, "your": 7}}) \ No newline at end of file +Search.setIndex({"alltitles": {"@copy": [[5, "copy"]], "@entry": [[5, "entry"]], "@env": [[5, "env"]], "@from": [[5, "from"]], "@label": [[5, "label"]], "@post": [[5, "post"]], "@pre": [[5, "pre"]], "@run": [[5, "run"]], "About": [[4, "about"]], "Adding Different Versions": [[8, "adding-different-versions"]], "Arguments": [[5, "arguments"]], "Base Image": [[8, "base-image"]], "Basic Usage": [[6, "basic-usage"]], "Build": [[1, "build"]], "Build Directory Layout": [[1, "build-directory-layout"]], "Build Process": [[1, null]], "Commandline Options": [[2, "commandline-options"]], "Conditionals": [[5, "conditionals"]], "Config Options": [[4, "config-options"]], "Configuration": [[2, null], [6, "configuration"]], "Configuration File": [[2, "configuration-file"]], "Create Build Script": [[1, "create-build-script"]], "Creating Your First Image": [[8, null]], "Files": [[1, "files"]], "Getting Started": [[7, null]], "Hello World!": [[8, "hello-world"]], "How it Works": [[6, "how-it-works"]], "Installation": [[6, "installation"]], "Overview": [[6, null]], "Parse Template": [[1, "parse-template"]], "Sections": [[5, "sections"]], "Setup": [[1, "setup"]], "Specifying Version": [[8, "specifying-version"]], "Specs.yaml": [[4, null]], "Technical Docs": [[3, null]], "Using when": [[4, "using-when"]], "VELOCITY_BACKEND": [[2, "velocity-backend"]], "VELOCITY_BUILD_DIR": [[2, "velocity-build-dir"]], "VELOCITY_CONFIG_DIR": [[2, "velocity-config-dir"]], "VELOCITY_DISTRO": [[2, "velocity-distro"]], "VELOCITY_IMAGE_PATH": [[2, "velocity-image-path"]], "VELOCITY_SYSTEM": [[2, "velocity-system"]], "VTMP Format": [[5, null]], "Variables": [[2, "variables"], [5, "variables"]], "Velocity Container Build Management": [[0, null]], "arguments": [[4, "arguments"]], "avail": [[6, "avail"]], "build": [[6, "build"]], "dependencies": [[4, "dependencies"]], "files": [[4, "files"]], "prologs": [[4, "prologs"]], "spec": [[6, "spec"]], "templates": [[4, "templates"]], "variables": [[4, "variables"]], "versions": [[4, "versions"]]}, "docnames": ["index", "reference/build", "reference/config", "reference/index", "reference/specs", "reference/vtmp", "starting/basic", "starting/index", "starting/tutorial"], "envversion": {"sphinx": 62, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst", "reference/build.rst", "reference/config.rst", "reference/index.rst", "reference/specs.rst", "reference/vtmp.rst", "starting/basic.rst", "starting/index.rst", "starting/tutorial.rst"], "indexentries": {}, "objects": {}, "objnames": {}, "objtypes": {}, "terms": {"": [6, 8], "0": [1, 4, 6, 8], "00": [6, 8], "00m00": 8, "00m01": 8, "00m02": 8, "00m03": 8, "01": [6, 8], "04": [5, 6], "07": 8, "08": 8, "09": 8, "0_fedora": 8, "1": [1, 4, 5, 6, 8], "10": [4, 6, 8], "100": 8, "104": 8, "11": [5, 6, 8], "114": 8, "117": 8, "12": [4, 6, 8], "125": 8, "129392640": 8, "13": [6, 8], "131072": 8, "132": 8, "133": 8, "134": 8, "135": 8, "139": 8, "14": [6, 8], "145": 8, "15": 8, "16": 8, "160": 8, "17": 8, "170": 8, "18": 8, "19": [6, 8], "190": 8, "190972": 8, "2": [4, 6, 8], "20": [5, 6, 8], "204": 8, "206": 8, "207": 8, "208": 8, "21": 8, "22": [6, 8], "228": 8, "23": [6, 8], "24": [6, 8], "25": 8, "256": 8, "257": 8, "26": 8, "269": 8, "27": 8, "273": 8, "274": 8, "276": 8, "28": 8, "280": 8, "29": 8, "291": 8, "293": 8, "3": [4, 6, 8], "309": 8, "31": 8, "310": 8, "32": 8, "320": 8, "33": 8, "34": 6, "34086d2996104518800c8d7dcc6139a1": 8, "35": 8, "354": 8, "36": 8, "37": 8, "38": 8, "385": 8, "38__x86_64": 8, "39": 8, "391": 8, "393": 8, "4": [4, 6, 8], "40": [6, 8], "408": 8, "4096": 8, "41": [1, 8], "41__x86_64": 8, "42": 8, "424": 8, "43": 8, "44": 8, "447": 8, "45": 4, "46": 4, "47": 4, "5": [6, 8], "50": 8, "503": 8, "52": 8, "523": 8, "524": 8, "547": 8, "583": 8, "6": [6, 8], "647": 8, "648": 8, "66007040": 8, "686": 8, "7": [5, 6, 8], "71": 8, "721": 8, "723": 8, "730": 8, "75": 8, "750": 8, "77": 8, "8": [6, 8], "810": 8, "83": 8, "887": 8, "89": 8, "8__x86_64__cento": 6, "8a9a360": [1, 8], "9": 8, "90": 8, "92": 8, "94": 8, "95": 8, "96": 6, "98": 8, "99": 8, "A": [2, 4, 5], "As": 2, "At": 1, "By": [2, 4], "For": 8, "If": [4, 5, 8], "In": [5, 6, 8], "It": [1, 2, 6], "NOT": 6, "One": 5, "That": 8, "The": [2, 4, 5, 6], "These": [2, 5], "To": [6, 8], "With": [1, 5], "__": 5, "__backend__": 5, "__base__": [5, 8], "__distro__": 5, "__name__": 5, "__system__": 5, "__timestamp__": 5, "__version__": [5, 8], "__version_major__": 5, "__version_minor__": 5, "__version_patch__": 5, "__version_suffix__": 5, "aa51aa7": 8, "abil": 5, "about": 3, "abov": 4, "across": 5, "ad": [2, 5, 7], "add": [4, 5, 8], "addit": 1, "addition": [2, 4], "after": [1, 5, 8], "alia": 6, "all": [5, 6, 8], "allow": 5, "also": [1, 5, 6, 8], "altern": 4, "an": [1, 2, 4, 5, 6, 8], "ani": [1, 5], "anyth": 5, "api": 5, "append": 1, "appli": 4, "approach": 6, "appropri": 5, "apptain": [0, 2, 4, 5, 6, 8], "ar": [2, 5, 6, 8], "arch": 8, "argument": [2, 3, 6], "assum": 6, "avail": [4, 5, 8], "b": [6, 8], "backend": [0, 2, 4, 5, 6, 8], "base": [5, 6, 7], "bash": [4, 5, 6, 8], "basic": 7, "been": 8, "befor": [4, 8], "begin": 5, "between": [5, 8], "bin": [5, 8], "bootstrap": 8, "both": [5, 6], "bsd": 1, "build": [2, 3, 4, 5, 8], "build_dir": [2, 6], "built": [2, 4, 6, 8], "cach": [2, 4, 8], "call": [1, 4, 8], "can": [2, 4, 5, 6, 8], "cannot": 4, "cd": 6, "cento": 6, "centos__stream8__x86_64__cento": 6, "certain": 5, "chain": 6, "charact": 5, "check": 8, "chmod": 8, "cisco": 8, "clean": 8, "cleanup": 8, "clone": [4, 6], "com": [6, 8], "come": 2, "command": [1, 2, 4, 5, 6, 8], "commandlin": 3, "comment": 5, "common": 8, "compat": 8, "complet": [2, 8], "complic": 8, "compon": [5, 6], "comput": 2, "condit": 3, "config": [2, 3, 6], "configur": [0, 3, 7, 8], "constraint": 2, "contain": [2, 5, 6, 8], "conveni": 6, "convert": 5, "copi": [1, 4, 8], "correct": 1, "creat": [0, 3, 6, 7], "critic": 6, "cuda": [5, 6], "cudnn": 6, "current": [4, 5], "d": 6, "data": 8, "de9c02b": [1, 8], "debug": [2, 6], "default": [2, 4, 8], "defin": [4, 5, 6], "definit": [2, 5, 6], "denot": 5, "depend": [5, 6, 8], "describ": 1, "dest": 5, "differ": [5, 7], "dir": [1, 5], "direct": 5, "directori": [2, 3, 4, 6, 8], "disabl": [2, 4, 8], "dist": 6, "distro": [0, 2, 4, 5, 6, 8], "dnf": 8, "do": 2, "doc": 0, "docker": [5, 8], "don": 5, "done": 5, "download": 8, "drwxr": 8, "drwxrwxrwt": 8, "e": [0, 2, 4, 5, 8], "each": [4, 6], "easiest": 6, "echo": 5, "edit": [6, 8], "els": 5, "empti": [5, 8], "enabl": 5, "end": [1, 5], "entri": 8, "entrypoint": 5, "env": 8, "envar": 5, "environ": [2, 5], "eras": 8, "error": [6, 8], "evalu": 4, "everi": [2, 4], "everyth": 5, "exampl": [2, 5], "example_arg": 5, "except": 4, "exhaust": 5, "exit": 6, "expat": 8, "extern": 5, "f": 8, "fail": 5, "fc": 8, "fc38": 8, "fc4": 8, "fc41": 8, "featur": [5, 6], "fedora": [1, 8], "few": 4, "file": [3, 5, 8], "filter": 4, "final": [1, 6], "first": [0, 5, 7], "flag": 8, "flexabl": 2, "folder": [4, 8], "follow": [5, 8], "forget": 5, "form": [5, 8], "format": [0, 3], "found": 5, "from": [2, 6, 8], "frontier": [2, 4, 6], "g": [0, 2, 4, 5, 8], "gcc": [4, 6], "gener": [1, 5, 6, 8], "get": [0, 5, 8], "git": [4, 6], "github": 6, "give": 8, "given": 6, "glibc": 8, "global": 2, "gnutl": 8, "go": 6, "goal": 5, "goe": 1, "good": 6, "got": 1, "great": 8, "greater": [4, 8], "h": 6, "ha": [6, 8], "hand": 6, "hash": 1, "have": [5, 8], "hello": [1, 5, 7], "hello_world": [1, 8], "help": [0, 2, 5, 6], "here": [2, 5], "highest": 2, "hostnam": [6, 8], "how": 7, "http": 6, "i": [0, 2, 4, 5, 6, 8], "iarfnxer": 6, "ident": 8, "imag": [0, 1, 2, 4, 5, 6, 7], "image_nam": 5, "image_path": [2, 6], "import": 6, "inbound": 8, "includ": 5, "indent": 6, "indic": 5, "info": [2, 6], "insert": 5, "instal": [7, 8], "intern": 5, "io": [5, 8], "item": 4, "json": 4, "k": 8, "kb": 8, "kei": 5, "kib": 8, "l": 8, "langpack": 8, "later": 8, "latest": [6, 8], "layout": 3, "less": 8, "let": 8, "level": [2, 6], "lib": 8, "libb2": 8, "libgcc": 8, "libgomp": 8, "libnghttp2": 8, "libstdc": 8, "libxcrypt": 8, "like": [1, 6, 8], "line": [2, 5], "list": [2, 4, 5, 6], "liter": 5, "littl": 8, "load": 8, "localhost": 6, "localimag": 8, "log": 1, "look": [1, 2, 4, 8], "lookup": 6, "lowest": 2, "lxogjapp": 6, "m": [6, 8], "maintain": 6, "mainten": 0, "major": 8, "make": 8, "manag": 6, "mani": 6, "mb": 8, "mean": 4, "messag": 6, "mib": 8, "miniforge3": 6, "minim": 8, "minor": 8, "modul": 6, "modular": 8, "more": [6, 8], "move": 8, "mpdecim": 8, "mpi": 6, "mpich": 6, "multipl": [0, 5], "myapp": 4, "name": [1, 2, 4, 5, 6, 8], "need": [5, 6, 8], "new": 8, "next": [1, 5], "ng": 8, "noarch": 8, "note": [4, 6], "notic": 8, "now": [6, 8], "number": [2, 4, 5, 6], "occur": [5, 8], "off": 6, "olcf": 6, "one": [5, 6], "ones": 6, "onli": [4, 5, 6], "openh264": 8, "openssl": 8, "oper": 8, "option": [3, 5, 6, 8], "orchestr": 6, "order": [1, 6, 8], "organiz": 2, "other": [1, 4, 5], "otherwis": [5, 6], "our": 8, "output": [1, 6, 8], "overview": [0, 7], "packag": [6, 8], "page": [1, 6], "pars": 3, "particular": [5, 6], "pass": 5, "patch": 8, "path": [2, 5, 6], "pbxpudvh": 6, "piec": 1, "pip": [6, 8], "place": [1, 2, 5], "podman": [0, 2, 4, 5], "point": [1, 2], "posit": 6, "post": 8, "pre": 8, "prebuilt": 6, "preced": 2, "preform": 2, "prepar": 8, "prerequist": 4, "previou": 5, "print": [6, 8], "process": [0, 3], "program": 6, "prolog": 1, "provid": [5, 6], "pull": [5, 8], "pure": 2, "purpos": 2, "py": [1, 8], "python": [6, 8], "python3": [6, 8], "python__3": 6, "pytorch": [4, 6], "r": 8, "rather": 6, "rc2": 8, "read": 4, "recommend": 6, "registri": 5, "relat": 5, "releas": 8, "remov": 8, "replac": [5, 8], "repo": 8, "repositori": [6, 8], "resolv": 8, "respect": 8, "result": 1, "right": 5, "rocm": 6, "root": 8, "rpm": 8, "run": [1, 4, 8], "runscript": [5, 8], "rwxr": 8, "sai": 4, "same": 5, "scope": 4, "scratch": [2, 6], "script": [0, 3, 4, 5, 6, 8], "scriptlet": 8, "second": 2, "second_tmp": 4, "section": [2, 3, 4], "see": [5, 6, 8], "select": 1, "sep": 8, "separ": [4, 5], "seper": [2, 6], "set": [2, 4, 5, 6, 8], "setup": 3, "shell": 5, "should": [1, 2, 5, 6, 8], "show": 6, "shown": 4, "sif": [1, 5, 8], "similar": 6, "simpl": 8, "size": 8, "so": [4, 5, 6, 8], "some": 8, "someth": 5, "somth": 1, "sourc": 6, "space": [2, 4, 5, 6], "spec": [0, 3, 5, 8], "specifi": [1, 2, 4, 5, 6, 7], "spectrum": 6, "split": 5, "src": 5, "start": [0, 5, 8], "statement": 4, "step": 1, "stop": 8, "stream8": 6, "string": 5, "structur": 6, "sub": 1, "substitut": 5, "succeed": 8, "success": 6, "suffix": 8, "summari": 8, "summit": 4, "sunflyhd": 6, "support": 5, "system": [0, 2, 4, 5, 6], "systemd": 8, "t": 5, "tab": 5, "take": [2, 5, 6, 8], "taken": 5, "technic": 0, "tell": 6, "templat": [3, 5, 6, 8], "template_vari": 5, "test": [4, 8], "test_var": [4, 5], "text": 5, "than": [4, 8], "thei": 5, "them": 1, "thi": [1, 2, 4, 5, 6, 8], "thing": 5, "those": 6, "three": 2, "through": 1, "time": [4, 5], "tmp": 8, "togeth": 1, "tool": [0, 6], "top": 5, "total": 8, "tpm2": 8, "trace": 6, "transact": 8, "tree": 6, "trigger": 8, "true": 5, "tss": 8, "tutori": 8, "ubuntu": [2, 4, 5, 6], "under": 5, "underneath": 6, "unifi": 5, "uninstal": 8, "unless": [2, 6], "unvers": 8, "up": [2, 5], "updat": 8, "upgrad": 8, "us": [2, 3, 5, 6, 8], "usag": 7, "user": [2, 5, 6, 8], "usr": 8, "v": [6, 8], "valu": [2, 4, 5], "variabl": 3, "veloc": [1, 2, 4, 5, 6, 8], "veri": 6, "verifi": 8, "version": [1, 5, 6, 7], "via": 8, "view": [2, 6], "vim": 8, "vtmp": [0, 3, 4, 8], "wa": 1, "wai": 6, "want": [4, 5, 8], "warn": 6, "we": [2, 6, 8], "weak": 8, "well": 5, "what": 2, "whatev": 8, "wheel": [6, 8], "when": [1, 2, 3, 5, 8], "where": [2, 5], "which": [4, 8], "while": 6, "white": 5, "whl": 6, "whole": 5, "wish": 8, "without": 5, "work": [5, 7], "world": [1, 5, 7], "would": 4, "write": 6, "written": 5, "x": 8, "x8": 8, "x86": 8, "x86_": 8, "x86_6": 8, "x86_64": 8, "xjv": 8, "xr": 8, "y": 8, "yaml": [0, 2, 3, 5, 6, 8], "you": [1, 2, 4, 5, 6, 8], "your": [0, 6, 7], "zlib": 8}, "titles": ["Velocity Container Build Management", "Build Process", "Configuration", "Technical Docs", "Specs.yaml", "VTMP Format", "Overview", "Getting Started", "Creating Your First Image"], "titleterms": {"about": 4, "ad": 8, "argument": [4, 5], "avail": 6, "base": 8, "basic": 6, "build": [0, 1, 6], "commandlin": 2, "condit": 5, "config": 4, "configur": [2, 6], "contain": 0, "copi": 5, "creat": [1, 8], "depend": 4, "differ": 8, "directori": 1, "doc": 3, "entri": 5, "env": 5, "file": [1, 2, 4], "first": 8, "format": 5, "from": 5, "get": 7, "hello": 8, "how": 6, "imag": 8, "instal": 6, "label": 5, "layout": 1, "manag": 0, "option": [2, 4], "overview": 6, "pars": 1, "post": 5, "pre": 5, "process": 1, "prolog": 4, "run": 5, "script": 1, "section": 5, "setup": 1, "spec": [4, 6], "specifi": 8, "start": 7, "technic": 3, "templat": [1, 4], "us": 4, "usag": 6, "variabl": [2, 4, 5], "veloc": 0, "velocity_backend": 2, "velocity_build_dir": 2, "velocity_config_dir": 2, "velocity_distro": 2, "velocity_image_path": 2, "velocity_system": 2, "version": [4, 8], "vtmp": 5, "when": 4, "work": 6, "world": 8, "yaml": 4, "your": 8}}) \ No newline at end of file diff --git a/docs/build/html/starting/basic.html b/docs/build/html/starting/basic.html index fbf768d..b6b8dc3 100644 --- a/docs/build/html/starting/basic.html +++ b/docs/build/html/starting/basic.html @@ -49,23 +49,13 @@
    • Getting Started
      • Overview @@ -105,172 +95,119 @@

        Overview

        -
        -

        Concept

        -

        Velocity is a tool to help with the maintenance of a variety of container builds on multiple systems, backends -(e.g podman or apptainer) and distros.

        -

        How it Works

        -

        Velocity works by building a set of containers in a chain so that the final container has all of the needed components.

        -
        -
        -

        Layout

        -

        The contents of the Velocity repo are fairly simple.

        -
        .
        -├── docs
        -├── lib
        -├── README.md
        -├── setup-env.sh
        -└── velocity
        -
        -
        -

        For informational purposes there is a folder docs which holds this documentation and a README file. -The setup-env.sh script can be used to ‘install’ velocity. The lib folder holds a python package which the -velocity script needs to run.

        +

        Velocity works by building a set of containers in a chain so that the final container has all of the needed components. +Velocity maintains a very hands off approach. It is only as good as the templates/configuration that you write. +In general it will assume that a particular build will work unless you tell it otherwise. It is important to note +that while Velocity has many features that are similar to those provided by a package manager, it is NOT a +package manager. Rather it should be viewed as a templating and build orchestration tool.

        Installation

        -

        First you will need to set up a conda environment for Velocity and install the following packages:

        -
        -

        Note

        -

        For more info on creating a custom conda environment on OLCF systems visit https://docs.olcf.ornl.gov/software/python/index.html#custom-environments.

        -
        -
        conda install pyyaml networkx colorama python-editor
        -
        -
        -
        -

        Important

        -

        If you wish to build the docs you will also need to install sphinx and sphinx-rtd-theme.

        -
        -

        Next clone the Velocity git repository to the desired location.

        -
        git clone https://gitlab.ccs.ornl.gov/saue-software/velocity.git
        +

        The easiest way to install velocity is to install prebuilt python packages using pip.

        +
        pip install olcf-velocity
         
        -

        You can then setup Velocity by sourcing the setup-env.sh script.

        -
        . ./velocity/setup-env.sh
        +

        You can also clone the velocity repository and build/install velocity from source.

        +
        git clone https://github.com/olcf/velocity.git
        +cd velocity
        +python3 -m build
        +# install the built python wheel package
        +pip install dist/olcf-velocity-<version>*.whl
         
        -

        The setup-env.sh script will help you choose the value of several environment variables -that velocity uses.

        -

        You should now be able to run the velocity command.

        -
        user@hostname:~$ velocity
        -==> System: x86_64
        -==> Backend: podman
        -==> Distro: fedora
        +

        Now you can use Velocity as a python module! We recommend setting a bash alias for convenience.

        +
        user@hostname:~$ alias velocity="python3 -m velocity"
        +user@hostname:~$ velocity
        +usage: velocity [-h] [-v] [-D {TRACE,DEBUG,INFO,SUCCESS,WARNING,ERROR,CRITICAL}] [-b BACKEND] [-s SYSTEM] [-d DISTRO] {build,avail,spec} ...
         
        -usage: velocity [-h] [-v] [-b BACKEND] [-s SYSTEM] [-d DISTRO]
        -                {build,avail,spec,edit} ...
        -
        -Build tool for OLCF containers
        +build tool for OLCF containers
         
         positional arguments:
        -  {build,avail,spec,edit}
        +  {build,avail,spec}
             build               build specified container image
             avail               lookup available images
             spec                lookup image dependencies
        -    edit                edit image files
         
         options:
           -h, --help            show this help message and exit
           -v, --version         program version
        +  -D {TRACE,DEBUG,INFO,SUCCESS,WARNING,ERROR,CRITICAL}, --debug {TRACE,DEBUG,INFO,SUCCESS,WARNING,ERROR,CRITICAL}
        +                        set debug output level
           -b BACKEND, --backend BACKEND
           -s SYSTEM, --system SYSTEM
           -d DISTRO, --distro DISTRO
         
        -See (https://gitlab.ccs.ornl.gov/saue-software/velocity)
        +See https://github.com/olcf/velocity
         

        Configuration

        -

        There are five system variables that need to be set for Velocity to work (these are set in the setup-env.sh script).

        -
        -

        VELOCITY_IMAGE_DIR

        -

        This variable points to the directory containing the the image definitions.

        +

        Velocity has a number of configuration options. The basic ones are setting the system name, container backend, +container distro, the path(s) to your image definitions and the build scratch directory. +To see more configuration options go to the configuration page. The easiest way to configure velocity is to +edit ~/.velocity/config.yaml.

        +
        velocity:
        +  system: frontier
        +  backend: apptainer
        +  distro: ubuntu
        +  image_path: # a list of : seperated paths
        +  build_dir: # path to a scratch space
        +
        +

        Note

        Image definitions can be created by the user as needed but a base set for usage at OLCF are provided at -https://gitlab.ccs.ornl.gov/saue-software/velocity-images.git

        +https://github.com/olcf/velocity-images

        -
        -

        VELOCITY_SYSTEM

        -

        This variable specifies what computer system you are building for (e.g. frontier).

        -
        -
        -

        VELOCITY_BACKEND

        -

        This variable specifies the container backend that should be used (e.g podman).

        -
        -
        -

        VELOCITY_DISTRO

        -

        This variable specifies the distro of the container images that will be built.

        -
        -
        -

        VELOCITY_BUILD_DIR

        -

        This variable specifies a scratch space for Velocity to preform builds in.

        -
        -

        Basic Usage

        avail

        The avail command prints the defined images that can be built.

        user@hostname:~$ velocity avail
        -==> System: x86_64
        -==> Backend: podman
        -==> Distro: centos
        -
        -==> centos
        -        stream8
         ==> gcc
        -        11.2.0
        -==> hmmer
        -        3.4
        -==> kalign
        -        3.4.0
        -==> miniforge3
        -        23.11.0
        -==> python
        -        3.11.8
        -==> pytorch
        -        latest
        +    12.3.0
        +    13.2.0
        +    14.1.0
        +==> mpich
        +    3.4.3
        +==> rocm
        +    5.7.1
        +==> ubuntu
        +    20.04
        +    22.04
        +    24.04
         
        -

        Each image is listed and then indented underneath is a list of the available versions -(in velocity they are called tags).

        +

        Each image is listed and then indented underneath is a list of the available versions.

        spec

        The spec command shows the dependencies for a given image (or list of images) in a tree like structure.

        -
        user@hostname:~$ velocity spec pytorch
        -==> System: summit
        -==> Backend: podman
        -==> Distro: centos
        -
        -  > pytorch@=latest
        -     ^cuda@=11.7.1
        -        ^centos@=stream8
        -     ^cudnn@=8.5.0.96
        -        ^cuda@=11.7.1
        -           ^centos@=stream8
        -     ^spectrum-mpi@=10.4.0.6
        -        ^centos@=stream8
        -     ^gcc@=11.2.0
        -        ^centos@=stream8
        -     ^miniforge3@=23.11.0
        -        ^centos@=stream8
        +
        user@hostname:~$ velocity spec pytorch
        +  > pytorch@latest
        +     ^cuda@11.7.1
        +        ^centos@stream8
        +     ^cudnn@8.5.0.96
        +        ^cuda@11.7.1
        +           ^centos@stream8
        +     ^spectrum-mpi@10.4.0.6
        +        ^centos@stream8
        +     ^gcc@11.2.0
        +        ^centos@stream8
        +     ^miniforge3@23.11.0
        +        ^centos@stream8
         

        build

        -

        The build can be used to build an container image from one or more image definitions.

        +

        The build command can be used to build an container image from one or more image definitions.

        user@hostname:~$ velocity build centos
        -==> System: x86_64
        -==> Backend: podman
        -==> Distro: centos
        -
         ==> Build Order:
                 centos@=stream8
         
        @@ -282,10 +219,6 @@ 

        build
        user@hostname:~$ velocity build gcc python
        -==> System: x86_64
        -==> Backend: podman
        -==> Distro: centos
        -
         ==> Build Order:
                 centos@=stream8
                 gcc@=11.2.0
        @@ -308,22 +241,6 @@ 

        build -

        edit

        -

        The edit command can be used to edit the VTMP or specification file for an image. By default -it edits the VTMP file. Add -s to edit the specifications.yaml file.

        -
        user@hostname:~$ velocity edit --help
        -usage: velocity edit [-h] [-s] target
        -
        -positional arguments:
        -  target               image to edit
        -
        -options:
        -  -h, --help           show this help message and exit
        -  -s, --specification  edit the specifications file
        -
        -
        -

        diff --git a/docs/build/html/starting/index.html b/docs/build/html/starting/index.html index e1d9292..646b622 100644 --- a/docs/build/html/starting/index.html +++ b/docs/build/html/starting/index.html @@ -23,7 +23,7 @@ - + @@ -86,16 +86,13 @@

        Getting Started

diff --git a/docs/build/html/starting/tutorial.html b/docs/build/html/starting/tutorial.html index ee81774..1af710d 100644 --- a/docs/build/html/starting/tutorial.html +++ b/docs/build/html/starting/tutorial.html @@ -50,7 +50,6 @@
  • Getting Started
    • Overview
    • Creating Your First Image
        -
      • Prerequisites
      • Base Image
      • Adding Different Versions
      • Specifying Version
      • @@ -91,426 +90,278 @@

        Creating Your First Image

        -
        -

        Prerequisites

        -

        You will need to have the Velocity repo cloned and the following environment variables set. You can do this by -sourcing the setup-env.sh script.

        -
        VELOCITY_IMAGE_DIR=<path to image dir>
        -VELOCITY_BACKEND=podman # if you use apptainer you will need to make changes to some of the examples
        -VELOCITY_BUILD_DIR=/tmp/velocity/build
        -VELOCITY_SYSTEM=x86_64  # if you use a different system name you will need to make changes to some of the examples
        -VELOCITY_ROOT=<git repo> # you will also need to add this to PATH
        -VELOCITY_DISTRO=fedora
        -
        -
        -

        Base Image

        -

        Let’s start with a simple base image. This image will pull an fedora docker image and update the packages. -Start by creating a directory in the image directory called fedora (for this tutorial we are starting with an empty -image directory). Next we need to create a directory in the fedora directory for the version of fedora that we want. -Let’s use 38. In this directory create a file called specifications.yaml and a directory called templates with -a file named fedora.vtmp. Your image directory and files should now look like this.

        +

        Let’s start with a simple base image. This image will pull an fedora docker image and update the packages. For this +tutorial I have created the empty directory /tmp/velocity/images and configured it as the image directory. I have set +the build directory to /tmp/velocity/build, the backend to apptainer and the distro as fedora. All +commands are run in /tmp/velocity. +Start by creating a directory in the image directory called fedora. In this directory create a file called +specs.yaml and a directory called templates with +a file named default.vtmp. Your image directory and files should now look like this.

        -
        VELOCITY_IMAGE_DIR
        -
        .
        -└── fedora
        -    └── 38
        -        ├── specifications.yaml
        -        └── templates
        -            └── fedora.vtmp
        +
        /tmp/velocity/images
        +
        fedora
        +├── specs.yaml
        +└── templates
        +    └── default.vtmp
         
        -
        fedora/38/specifications.yaml
        -
        build_specifications:
        -
        -  x86_64:
        -    podman:
        -      fedora: {}
        +
        specs.yaml
        +
        versions:
        +  - spec: 38
        +    when: distro=fedora
         
        -
        fedora/38/templates/fedora.vtmp
        +
        default.vtmp
        @from
        -    docker.io/fedora:38
        +    docker.io/fedora:{{ __version__ }}
         
         @run
             dnf -y upgrade
        -
        -@label
        -    velocity.config.system %(__system__)
        -    velocity.config.backend %(__backend__)
        -    velocity.config.distro %(__distro__)
        -    velocity.image.%(__name__)__%(__tag__) %(__hash__)
        +    dnf clean all
         

        Now if you run velocity avail you should get the following.

        user@hostname:~$ velocity avail
        -==> System: x86_64
        -==> Backend: podman
        -==> Distro: fedora
        -
         ==> fedora
        -        38
        +    38
         

        Now build the image.

        user@hostname:~$ velocity build fedora
        -==> System: x86_64
        -==> Backend: podman
        -==> Distro: fedora
        -
         ==> Build Order:
        -        fedora@=38
        +    fedora@38-aa51aa7
        +
        +==> aa51aa7: BUILD fedora@38 ...
        +==> aa51aa7: GENERATING SCRIPT ...
        +==> aa51aa7: BUILDING ...
        +==> aa51aa7: IMAGE /tmp/velocity/build/fedora-38-aa51aa7/aa51aa7.sif (fedora@38) BUILT [0:01:07]
         
        -==> yftozouc: BUILD fedora@=38 ...
        -==> yftozouc: GENERATING SCRIPT ...
        -==> yftozouc: BUILDING ...
        -==> yftozouc: IMAGE localhost/fedora__38__x86_64__fedora:latest (fedora@=38) BUILT [0:07:10]
        +==> BUILT: /tmp/velocity/fedora-38__x86_64-fedora.sif
         

        If you wish to see more output you can add the -v flag:

        user@hostname:~$ velocity build fedora -v
        -==> System: x86_64
        -==> Backend: podman
        -==> Distro: fedora
        -
         ==> Build Order:
        -        fedora@=38
        -
        -==> xuoykrdt: BUILD fedora@=38 ...
        -==> xuoykrdt: GENERATING SCRIPT ...
        -        SCRIPT: /tmp/velocity/build/xuoykrdt/script
        -        FROM docker.io/fedora:38
        -
        -        RUN dnf -y upgrade
        -
        -        LABEL velocity.config.system="x86_64" \
        -            velocity.config.backend="podman" \
        -            velocity.config.distro="fedora" \
        -            velocity.image.fedora__38="ff9fa85cf102560cf3fe2014c3c758fbb3809247537abbeab2c4b67c62dda164"
        -
        -==> xuoykrdt: BUILDING ...
        -        #!/usr/bin/env bash
        -        podman build -f /tmp/velocity/build/xuoykrdt/script -t localhost/fedora__38__x86_64__fedora:latest .;
        -        STEP 1/3: FROM docker.io/fedora:38
        -        STEP 2/3: RUN dnf -y upgrade
        -        Fedora 38 - x86_64                              131 kB/s |  84 MB     10:53
        -        Fedora 38 openh264 (From Cisco) - x86_64        2.9 kB/s | 2.6 kB     00:00
        -        Fedora Modular 38 - x86_64                      1.1 MB/s | 2.8 MB     00:02
        -        Fedora 38 - x86_64 - Updates                    4.5 MB/s |  40 MB     00:09
        -        Fedora Modular 38 - x86_64 - Updates            152 kB/s | 2.1 MB     00:14
        -        Last metadata expiration check: 0:00:01 ago on Wed Mar 27 20:09:54 2024.
        -        Dependencies resolved.
        -        ================================================================================
        -         Package                       Arch     Version                 Repo       Size
        -        ================================================================================
        -        Upgrading:
        -         curl                          x86_64   8.0.1-7.fc38            updates   348 k
        -         dnf                           noarch   4.19.0-1.fc38           updates   507 k
        -         dnf-data                      noarch   4.19.0-1.fc38           updates    39 k
        -         elfutils-default-yama-scope   noarch   0.191-1.fc38            updates    12 k
        -         elfutils-libelf               x86_64   0.191-1.fc38            updates   208 k
        -         elfutils-libs                 x86_64   0.191-1.fc38            updates   263 k
        -         expat                         x86_64   2.6.0-1.fc38            updates   112 k
        -         keyutils-libs                 x86_64   1.6.3-1.fc38            updates    31 k
        -         libcurl                       x86_64   8.0.1-7.fc38            updates   315 k
        -         libdnf                        x86_64   0.73.0-1.fc38           updates   681 k
        -         libgcc                        x86_64   13.2.1-7.fc38           updates   115 k
        -         libgomp                       x86_64   13.2.1-7.fc38           updates   324 k
        -         libsolv                       x86_64   0.7.28-1.fc38           updates   426 k
        -         libstdc++                     x86_64   13.2.1-7.fc38           updates   870 k
        -         ncurses-base                  noarch   6.4-7.20230520.fc38.1   updates    88 k
        -         ncurses-libs                  x86_64   6.4-7.20230520.fc38.1   updates   336 k
        -         python3                       x86_64   3.11.8-2.fc38           updates    28 k
        -         python3-dnf                   noarch   4.19.0-1.fc38           updates   606 k
        -         python3-hawkey                x86_64   0.73.0-1.fc38           updates   107 k
        -         python3-libdnf                x86_64   0.73.0-1.fc38           updates   859 k
        -         python3-libs                  x86_64   3.11.8-2.fc38           updates   9.6 M
        -         systemd-libs                  x86_64   253.17-1.fc38           updates   649 k
        -         vim-data                      noarch   2:9.1.158-1.fc38        updates    23 k
        -         vim-minimal                   x86_64   2:9.1.158-1.fc38        updates   808 k
        -         yum                           noarch   4.19.0-1.fc38           updates    37 k
        -
        -        Transaction Summary
        -        ================================================================================
        -        Upgrade  25 Packages
        -
        -        Total download size: 17 M
        -        Downloading Packages:
        -        (1/25): dnf-data-4.19.0-1.fc38.noarch.rpm       107 kB/s |  39 kB     00:00
        -        (2/25): elfutils-default-yama-scope-0.191-1.fc3 224 kB/s |  12 kB     00:00
        -        (3/25): curl-8.0.1-7.fc38.x86_64.rpm            637 kB/s | 348 kB     00:00
        -        (4/25): dnf-4.19.0-1.fc38.noarch.rpm            807 kB/s | 507 kB     00:00
        -        (5/25): elfutils-libelf-0.191-1.fc38.x86_64.rpm 901 kB/s | 208 kB     00:00
        -        (6/25): expat-2.6.0-1.fc38.x86_64.rpm           1.1 MB/s | 112 kB     00:00
        -        (7/25): keyutils-libs-1.6.3-1.fc38.x86_64.rpm   331 kB/s |  31 kB     00:00
        -        (8/25): elfutils-libs-0.191-1.fc38.x86_64.rpm   1.1 MB/s | 263 kB     00:00
        -        (9/25): libgcc-13.2.1-7.fc38.x86_64.rpm         872 kB/s | 115 kB     00:00
        -        (10/25): libcurl-8.0.1-7.fc38.x86_64.rpm        1.7 MB/s | 315 kB     00:00
        -        (11/25): libgomp-13.2.1-7.fc38.x86_64.rpm       1.8 MB/s | 324 kB     00:00
        -        (12/25): libdnf-0.73.0-1.fc38.x86_64.rpm        1.8 MB/s | 681 kB     00:00
        -        (13/25): libsolv-0.7.28-1.fc38.x86_64.rpm       2.1 MB/s | 426 kB     00:00
        -        (14/25): ncurses-base-6.4-7.20230520.fc38.1.noa 423 kB/s |  88 kB     00:00
        -        (15/25): ncurses-libs-6.4-7.20230520.fc38.1.x86 1.3 MB/s | 336 kB     00:00
        -        (16/25): python3-3.11.8-2.fc38.x86_64.rpm       286 kB/s |  28 kB     00:00
        -        (17/25): libstdc++-13.2.1-7.fc38.x86_64.rpm     1.8 MB/s | 870 kB     00:00
        -        (18/25): python3-hawkey-0.73.0-1.fc38.x86_64.rp 834 kB/s | 107 kB     00:00
        -        (19/25): python3-dnf-4.19.0-1.fc38.noarch.rpm   2.5 MB/s | 606 kB     00:00
        -        (20/25): python3-libdnf-0.73.0-1.fc38.x86_64.rp 1.6 MB/s | 859 kB     00:00
        -        (21/25): systemd-libs-253.17-1.fc38.x86_64.rpm  1.4 MB/s | 649 kB     00:00
        -        (22/25): vim-data-9.1.158-1.fc38.noarch.rpm     196 kB/s |  23 kB     00:00
        -        (23/25): yum-4.19.0-1.fc38.noarch.rpm           270 kB/s |  37 kB     00:00
        -        (24/25): vim-minimal-9.1.158-1.fc38.x86_64.rpm  2.5 MB/s | 808 kB     00:00
        -        (25/25): python3-libs-3.11.8-2.fc38.x86_64.rpm  3.6 MB/s | 9.6 MB     00:02
        -        --------------------------------------------------------------------------------
        -        Total                                           3.6 MB/s |  17 MB     00:04
        -        Running transaction check
        -        Transaction check succeeded.
        -        Running transaction test
        -        Transaction test succeeded.
        -        Running transaction
        -          Preparing        :                                                        1/1
        -          Upgrading        : libgcc-13.2.1-7.fc38.x86_64                           1/50
        -          Running scriptlet: libgcc-13.2.1-7.fc38.x86_64                           1/50
        -          Upgrading        : libstdc++-13.2.1-7.fc38.x86_64                        2/50
        -          Upgrading        : libsolv-0.7.28-1.fc38.x86_64                          3/50
        -          Upgrading        : libdnf-0.73.0-1.fc38.x86_64                           4/50
        -          Upgrading        : vim-data-2:9.1.158-1.fc38.noarch                      5/50
        -          Upgrading        : ncurses-base-6.4-7.20230520.fc38.1.noarch             6/50
        -          Upgrading        : ncurses-libs-6.4-7.20230520.fc38.1.x86_64             7/50
        -          Upgrading        : libcurl-8.0.1-7.fc38.x86_64                           8/50
        -          Upgrading        : expat-2.6.0-1.fc38.x86_64                             9/50
        -          Upgrading        : python3-3.11.8-2.fc38.x86_64                         10/50
        -          Upgrading        : python3-libs-3.11.8-2.fc38.x86_64                    11/50
        -          Upgrading        : python3-libdnf-0.73.0-1.fc38.x86_64                  12/50
        -          Upgrading        : python3-hawkey-0.73.0-1.fc38.x86_64                  13/50
        -          Upgrading        : elfutils-libelf-0.191-1.fc38.x86_64                  14/50
        -          Upgrading        : elfutils-default-yama-scope-0.191-1.fc38.noarch      15/50
        -          Running scriptlet: elfutils-default-yama-scope-0.191-1.fc38.noarch      15/50
        -          Upgrading        : dnf-data-4.19.0-1.fc38.noarch                        16/50
        -          Upgrading        : python3-dnf-4.19.0-1.fc38.noarch                     17/50
        -          Upgrading        : dnf-4.19.0-1.fc38.noarch                             18/50
        -          Running scriptlet: dnf-4.19.0-1.fc38.noarch                             18/50
        -          Upgrading        : yum-4.19.0-1.fc38.noarch                             19/50
        -          Upgrading        : elfutils-libs-0.191-1.fc38.x86_64                    20/50
        -          Upgrading        : curl-8.0.1-7.fc38.x86_64                             21/50
        -          Upgrading        : vim-minimal-2:9.1.158-1.fc38.x86_64                  22/50
        -          Upgrading        : systemd-libs-253.17-1.fc38.x86_64                    23/50
        -          Upgrading        : libgomp-13.2.1-7.fc38.x86_64                         24/50
        -          Upgrading        : keyutils-libs-1.6.3-1.fc38.x86_64                    25/50
        -          Cleanup          : elfutils-libs-0.190-2.fc38.x86_64                    26/50
        -          Cleanup          : systemd-libs-253.15-2.fc38.x86_64                    27/50
        -          Cleanup          : vim-minimal-2:9.1.113-1.fc38.x86_64                  28/50
        -          Cleanup          : curl-8.0.1-6.fc38.x86_64                             29/50
        -          Cleanup          : yum-4.18.2-1.fc38.noarch                             30/50
        -          Running scriptlet: dnf-4.18.2-1.fc38.noarch                             31/50
        -          Cleanup          : dnf-4.18.2-1.fc38.noarch                             31/50
        -          Running scriptlet: dnf-4.18.2-1.fc38.noarch                             31/50
        -          Cleanup          : python3-dnf-4.18.2-1.fc38.noarch                     32/50
        -          Cleanup          : dnf-data-4.18.2-1.fc38.noarch                        33/50
        -          Cleanup          : vim-data-2:9.1.113-1.fc38.noarch                     34/50
        -          Cleanup          : elfutils-default-yama-scope-0.190-2.fc38.noarch      35/50
        -          Cleanup          : python3-hawkey-0.72.0-1.fc38.x86_64                  36/50
        -          Cleanup          : python3-libdnf-0.72.0-1.fc38.x86_64                  37/50
        -          Cleanup          : libdnf-0.72.0-1.fc38.x86_64                          38/50
        -          Cleanup          : libstdc++-13.2.1-4.fc38.x86_64                       39/50
        -          Cleanup          : python3-libs-3.11.7-2.fc38.x86_64                    40/50
        -          Cleanup          : python3-3.11.7-2.fc38.x86_64                         41/50
        -          Cleanup          : ncurses-libs-6.4-7.20230520.fc38.x86_64              42/50
        -          Cleanup          : ncurses-base-6.4-7.20230520.fc38.noarch              43/50
        -          Cleanup          : expat-2.5.0-2.fc38.x86_64                            44/50
        -          Cleanup          : libgcc-13.2.1-4.fc38.x86_64                          45/50
        -          Running scriptlet: libgcc-13.2.1-4.fc38.x86_64                          45/50
        -          Cleanup          : libsolv-0.7.27-1.fc38.x86_64                         46/50
        -          Cleanup          : libcurl-8.0.1-6.fc38.x86_64                          47/50
        -          Cleanup          : elfutils-libelf-0.190-2.fc38.x86_64                  48/50
        -          Cleanup          : libgomp-13.2.1-4.fc38.x86_64                         49/50
        -          Cleanup          : keyutils-libs-1.6.1-6.fc38.x86_64                    50/50
        -          Running scriptlet: keyutils-libs-1.6.1-6.fc38.x86_64                    50/50
        -          Verifying        : curl-8.0.1-7.fc38.x86_64                              1/50
        -          Verifying        : curl-8.0.1-6.fc38.x86_64                              2/50
        -          Verifying        : dnf-4.19.0-1.fc38.noarch                              3/50
        -          Verifying        : dnf-4.18.2-1.fc38.noarch                              4/50
        -          Verifying        : dnf-data-4.19.0-1.fc38.noarch                         5/50
        -          Verifying        : dnf-data-4.18.2-1.fc38.noarch                         6/50
        -          Verifying        : elfutils-default-yama-scope-0.191-1.fc38.noarch       7/50
        -          Verifying        : elfutils-default-yama-scope-0.190-2.fc38.noarch       8/50
        -          Verifying        : elfutils-libelf-0.191-1.fc38.x86_64                   9/50
        -          Verifying        : elfutils-libelf-0.190-2.fc38.x86_64                  10/50
        -          Verifying        : elfutils-libs-0.191-1.fc38.x86_64                    11/50
        -          Verifying        : elfutils-libs-0.190-2.fc38.x86_64                    12/50
        -          Verifying        : expat-2.6.0-1.fc38.x86_64                            13/50
        -          Verifying        : expat-2.5.0-2.fc38.x86_64                            14/50
        -          Verifying        : keyutils-libs-1.6.3-1.fc38.x86_64                    15/50
        -          Verifying        : keyutils-libs-1.6.1-6.fc38.x86_64                    16/50
        -          Verifying        : libcurl-8.0.1-7.fc38.x86_64                          17/50
        -          Verifying        : libcurl-8.0.1-6.fc38.x86_64                          18/50
        -          Verifying        : libdnf-0.73.0-1.fc38.x86_64                          19/50
        -          Verifying        : libdnf-0.72.0-1.fc38.x86_64                          20/50
        -          Verifying        : libgcc-13.2.1-7.fc38.x86_64                          21/50
        -          Verifying        : libgcc-13.2.1-4.fc38.x86_64                          22/50
        -          Verifying        : libgomp-13.2.1-7.fc38.x86_64                         23/50
        -          Verifying        : libgomp-13.2.1-4.fc38.x86_64                         24/50
        -          Verifying        : libsolv-0.7.28-1.fc38.x86_64                         25/50
        -          Verifying        : libsolv-0.7.27-1.fc38.x86_64                         26/50
        -          Verifying        : libstdc++-13.2.1-7.fc38.x86_64                       27/50
        -          Verifying        : libstdc++-13.2.1-4.fc38.x86_64                       28/50
        -          Verifying        : ncurses-base-6.4-7.20230520.fc38.1.noarch            29/50
        -          Verifying        : ncurses-base-6.4-7.20230520.fc38.noarch              30/50
        -          Verifying        : ncurses-libs-6.4-7.20230520.fc38.1.x86_64            31/50
        -          Verifying        : ncurses-libs-6.4-7.20230520.fc38.x86_64              32/50
        -          Verifying        : python3-3.11.8-2.fc38.x86_64                         33/50
        -          Verifying        : python3-3.11.7-2.fc38.x86_64                         34/50
        -          Verifying        : python3-dnf-4.19.0-1.fc38.noarch                     35/50
        -          Verifying        : python3-dnf-4.18.2-1.fc38.noarch                     36/50
        -          Verifying        : python3-hawkey-0.73.0-1.fc38.x86_64                  37/50
        -          Verifying        : python3-hawkey-0.72.0-1.fc38.x86_64                  38/50
        -          Verifying        : python3-libdnf-0.73.0-1.fc38.x86_64                  39/50
        -          Verifying        : python3-libdnf-0.72.0-1.fc38.x86_64                  40/50
        -          Verifying        : python3-libs-3.11.8-2.fc38.x86_64                    41/50
        -          Verifying        : python3-libs-3.11.7-2.fc38.x86_64                    42/50
        -          Verifying        : systemd-libs-253.17-1.fc38.x86_64                    43/50
        -          Verifying        : systemd-libs-253.15-2.fc38.x86_64                    44/50
        -          Verifying        : vim-data-2:9.1.158-1.fc38.noarch                     45/50
        -          Verifying        : vim-data-2:9.1.113-1.fc38.noarch                     46/50
        -          Verifying        : vim-minimal-2:9.1.158-1.fc38.x86_64                  47/50
        -          Verifying        : vim-minimal-2:9.1.113-1.fc38.x86_64                  48/50
        -          Verifying        : yum-4.19.0-1.fc38.noarch                             49/50
        -          Verifying        : yum-4.18.2-1.fc38.noarch                             50/50
        -
        -        Upgraded:
        -          curl-8.0.1-7.fc38.x86_64
        -          dnf-4.19.0-1.fc38.noarch
        -          dnf-data-4.19.0-1.fc38.noarch
        -          elfutils-default-yama-scope-0.191-1.fc38.noarch
        -          elfutils-libelf-0.191-1.fc38.x86_64
        -          elfutils-libs-0.191-1.fc38.x86_64
        -          expat-2.6.0-1.fc38.x86_64
        -          keyutils-libs-1.6.3-1.fc38.x86_64
        -          libcurl-8.0.1-7.fc38.x86_64
        -          libdnf-0.73.0-1.fc38.x86_64
        -          libgcc-13.2.1-7.fc38.x86_64
        -          libgomp-13.2.1-7.fc38.x86_64
        -          libsolv-0.7.28-1.fc38.x86_64
        -          libstdc++-13.2.1-7.fc38.x86_64
        -          ncurses-base-6.4-7.20230520.fc38.1.noarch
        -          ncurses-libs-6.4-7.20230520.fc38.1.x86_64
        -          python3-3.11.8-2.fc38.x86_64
        -          python3-dnf-4.19.0-1.fc38.noarch
        -          python3-hawkey-0.73.0-1.fc38.x86_64
        -          python3-libdnf-0.73.0-1.fc38.x86_64
        -          python3-libs-3.11.8-2.fc38.x86_64
        -          systemd-libs-253.17-1.fc38.x86_64
        -          vim-data-2:9.1.158-1.fc38.noarch
        -          vim-minimal-2:9.1.158-1.fc38.x86_64
        -          yum-4.19.0-1.fc38.noarch
        -
        -        Complete!
        -        --> 787eba03a2e
        -        STEP 3/3: LABEL velocity.config.system="x86_64"     velocity.config.backend="podman"     velocity.config.distro="fedora"     velocity.image.fedora__38="ff9fa85cf102560cf3fe2014c3c758fbb3809247537abbeab2c4b67c62dda164"
        -        COMMIT localhost/fedora__38__x86_64__fedora:latest
        -        --> 17c3457c281
        -        Successfully tagged localhost/fedora__38__x86_64__fedora:latest
        -        17c3457c281309909691b183b77323eb56390792a787e4a319b494d40868c907
        -==> xuoykrdt: IMAGE localhost/fedora__38__x86_64__fedora:latest (fedora@=38) BUILT [0:13:27]
        +    fedora@38-aa51aa7
        +
        +==> aa51aa7: BUILD fedora@38 ...
        +==> aa51aa7: GENERATING SCRIPT ...
        +    SCRIPT: /tmp/velocity/build/fedora-38-aa51aa7/script
        +    Bootstrap: docker
        +    From: docker.io/fedora:38
        +
        +    %post
        +    dnf -y upgrade
        +    dnf clean all
        +==> aa51aa7: BUILDING ...
        +    #!/usr/bin/env bash
        +    apptainer build --disable-cache /tmp/velocity/build/fedora-38-aa51aa7/aa51aa7.sif /tmp/velocity/build/fedora-38-aa51aa7/script;
        +    Fedora 38 - x86_64                              2.3 MB/s |  83 MB     00:35
        +    Fedora 38 openh264 (From Cisco) - x86_64        2.8 kB/s | 2.6 kB     00:00
        +    Fedora Modular 38 - x86_64                      1.8 MB/s | 2.8 MB     00:01
        +    Fedora 38 - x86_64 - Updates                    2.8 MB/s |  42 MB     00:14
        +    Fedora Modular 38 - x86_64 - Updates            257 kB/s | 2.2 MB     00:08
        +    Dependencies resolved.
        +    ================================================================================
        +     Package                            Arch    Version              Repo      Size
        +    ================================================================================
        +    Upgrading:
        +     fedora-release-common              noarch  38-37                updates   20 k
        +     fedora-release-container           noarch  38-37                updates   10 k
        +     fedora-release-identity-container  noarch  38-37                updates   12 k
        +     glibc                              x86_64  2.37-19.fc38         updates  2.1 M
        +     glibc-common                       x86_64  2.37-19.fc38         updates  320 k
        +     glibc-minimal-langpack             x86_64  2.37-19.fc38         updates   42 k
        +     gnutls                             x86_64  3.8.5-1.fc38         updates  1.1 M
        +     libnghttp2                         x86_64  1.52.0-3.fc38        updates   75 k
        +     python-pip-wheel                   noarch  22.3.1-4.fc38        updates  1.4 M
        +     python3                            x86_64  3.11.9-2.fc38        updates   28 k
        +     python3-libs                       x86_64  3.11.9-2.fc38        updates  9.6 M
        +     tpm2-tss                           x86_64  4.0.2-1.fc38         updates  391 k
        +     vim-data                           noarch  2:9.1.393-1.fc38     updates   23 k
        +     vim-minimal                        x86_64  2:9.1.393-1.fc38     updates  810 k
        +    Installing weak dependencies:
        +     libxcrypt-compat                   x86_64  4.4.36-1.fc38        updates   90 k
        +
        +    Transaction Summary
        +    ================================================================================
        +    Install   1 Package
        +    Upgrade  14 Packages
        +
        +    Total download size: 16 M
        +    Downloading Packages:
        +    (1/15): fedora-release-container-38-37.noarch.r 160 kB/s |  10 kB     00:00
        +    (2/15): fedora-release-common-38-37.noarch.rpm  228 kB/s |  20 kB     00:00
        +    (3/15): fedora-release-identity-container-38-37 385 kB/s |  12 kB     00:00
        +    (4/15): libxcrypt-compat-4.4.36-1.fc38.x86_64.r 648 kB/s |  90 kB     00:00
        +    (5/15): glibc-minimal-langpack-2.37-19.fc38.x86 1.1 MB/s |  42 kB     00:00
        +    (6/15): glibc-common-2.37-19.fc38.x86_64.rpm    2.4 MB/s | 320 kB     00:00
        +    (7/15): libnghttp2-1.52.0-3.fc38.x86_64.rpm     1.7 MB/s |  75 kB     00:00
        +    (8/15): glibc-2.37-19.fc38.x86_64.rpm           8.1 MB/s | 2.1 MB     00:00
        +    (9/15): python3-3.11.9-2.fc38.x86_64.rpm        686 kB/s |  28 kB     00:00
        +    (10/15): python-pip-wheel-22.3.1-4.fc38.noarch. 8.8 MB/s | 1.4 MB     00:00
        +    (11/15): tpm2-tss-4.0.2-1.fc38.x86_64.rpm       6.1 MB/s | 391 kB     00:00
        +    (12/15): gnutls-3.8.5-1.fc38.x86_64.rpm         3.1 MB/s | 1.1 MB     00:00
        +    (13/15): vim-data-9.1.393-1.fc38.noarch.rpm     503 kB/s |  23 kB     00:00
        +    (14/15): vim-minimal-9.1.393-1.fc38.x86_64.rpm  4.3 MB/s | 810 kB     00:00
        +    (15/15): python3-libs-3.11.9-2.fc38.x86_64.rpm   11 MB/s | 9.6 MB     00:00
        +    --------------------------------------------------------------------------------
        +    Total                                            11 MB/s |  16 MB     00:01
        +    Running transaction check
        +    Transaction check succeeded.
        +    Running transaction test
        +    Transaction test succeeded.
        +    Running transaction
        +      Preparing        :                                                        1/1
        +      Upgrading        : glibc-common-2.37-19.fc38.x86_64                      1/29
        +      Upgrading        : glibc-minimal-langpack-2.37-19.fc38.x86_64            2/29
        +      Running scriptlet: glibc-2.37-19.fc38.x86_64                             3/29
        +      Upgrading        : glibc-2.37-19.fc38.x86_64                             3/29
        +      Running scriptlet: glibc-2.37-19.fc38.x86_64                             3/29
        +      Upgrading        : fedora-release-identity-container-38-37.noarch        4/29
        +      Upgrading        : fedora-release-container-38-37.noarch                 5/29
        +      Upgrading        : fedora-release-common-38-37.noarch                    6/29
        +      Installing       : libxcrypt-compat-4.4.36-1.fc38.x86_64                 7/29
        +      Upgrading        : python-pip-wheel-22.3.1-4.fc38.noarch                 8/29
        +      Upgrading        : python3-3.11.9-2.fc38.x86_64                          9/29
        +      Upgrading        : python3-libs-3.11.9-2.fc38.x86_64                    10/29
        +      Upgrading        : vim-data-2:9.1.393-1.fc38.noarch                     11/29
        +      Upgrading        : vim-minimal-2:9.1.393-1.fc38.x86_64                  12/29
        +      Upgrading        : gnutls-3.8.5-1.fc38.x86_64                           13/29
        +      Upgrading        : libnghttp2-1.52.0-3.fc38.x86_64                      14/29
        +      Running scriptlet: tpm2-tss-4.0.2-1.fc38.x86_64                         15/29
        +      Upgrading        : tpm2-tss-4.0.2-1.fc38.x86_64                         15/29
        +      Cleanup          : fedora-release-common-38-36.noarch                   16/29
        +      Cleanup          : gnutls-3.8.4-1.fc38.x86_64                           17/29
        +      Cleanup          : tpm2-tss-4.0.1-3.fc38.x86_64                         18/29
        +      Cleanup          : vim-minimal-2:9.1.309-1.fc38.x86_64                  19/29
        +      Cleanup          : libnghttp2-1.52.0-2.fc38.x86_64                      20/29
        +      Cleanup          : python3-3.11.8-2.fc38.x86_64                         21/29
        +      Cleanup          : fedora-release-container-38-36.noarch                22/29
        +      Cleanup          : fedora-release-identity-container-38-36.noarch       23/29
        +      Cleanup          : vim-data-2:9.1.309-1.fc38.noarch                     24/29
        +      Cleanup          : python3-libs-3.11.8-2.fc38.x86_64                    25/29
        +      Cleanup          : python-pip-wheel-22.3.1-3.fc38.noarch                26/29
        +      Cleanup          : glibc-2.37-18.fc38.x86_64                            27/29
        +      Cleanup          : glibc-minimal-langpack-2.37-18.fc38.x86_64           28/29
        +      Cleanup          : glibc-common-2.37-18.fc38.x86_64                     29/29
        +      Running scriptlet: glibc-common-2.37-18.fc38.x86_64                     29/29
        +      Verifying        : libxcrypt-compat-4.4.36-1.fc38.x86_64                 1/29
        +      Verifying        : fedora-release-common-38-37.noarch                    2/29
        +      Verifying        : fedora-release-common-38-36.noarch                    3/29
        +      Verifying        : fedora-release-container-38-37.noarch                 4/29
        +      Verifying        : fedora-release-container-38-36.noarch                 5/29
        +      Verifying        : fedora-release-identity-container-38-37.noarch        6/29
        +      Verifying        : fedora-release-identity-container-38-36.noarch        7/29
        +      Verifying        : glibc-2.37-19.fc38.x86_64                             8/29
        +      Verifying        : glibc-2.37-18.fc38.x86_64                             9/29
        +      Verifying        : glibc-common-2.37-19.fc38.x86_64                     10/29
        +      Verifying        : glibc-common-2.37-18.fc38.x86_64                     11/29
        +      Verifying        : glibc-minimal-langpack-2.37-19.fc38.x86_64           12/29
        +      Verifying        : glibc-minimal-langpack-2.37-18.fc38.x86_64           13/29
        +      Verifying        : gnutls-3.8.5-1.fc38.x86_64                           14/29
        +      Verifying        : gnutls-3.8.4-1.fc38.x86_64                           15/29
        +      Verifying        : libnghttp2-1.52.0-3.fc38.x86_64                      16/29
        +      Verifying        : libnghttp2-1.52.0-2.fc38.x86_64                      17/29
        +      Verifying        : python-pip-wheel-22.3.1-4.fc38.noarch                18/29
        +      Verifying        : python-pip-wheel-22.3.1-3.fc38.noarch                19/29
        +      Verifying        : python3-3.11.9-2.fc38.x86_64                         20/29
        +      Verifying        : python3-3.11.8-2.fc38.x86_64                         21/29
        +      Verifying        : python3-libs-3.11.9-2.fc38.x86_64                    22/29
        +      Verifying        : python3-libs-3.11.8-2.fc38.x86_64                    23/29
        +      Verifying        : tpm2-tss-4.0.2-1.fc38.x86_64                         24/29
        +      Verifying        : tpm2-tss-4.0.1-3.fc38.x86_64                         25/29
        +      Verifying        : vim-data-2:9.1.393-1.fc38.noarch                     26/29
        +      Verifying        : vim-data-2:9.1.309-1.fc38.noarch                     27/29
        +      Verifying        : vim-minimal-2:9.1.393-1.fc38.x86_64                  28/29
        +      Verifying        : vim-minimal-2:9.1.309-1.fc38.x86_64                  29/29
        +
        +    Upgraded:
        +      fedora-release-common-38-37.noarch
        +      fedora-release-container-38-37.noarch
        +      fedora-release-identity-container-38-37.noarch
        +      glibc-2.37-19.fc38.x86_64
        +      glibc-common-2.37-19.fc38.x86_64
        +      glibc-minimal-langpack-2.37-19.fc38.x86_64
        +      gnutls-3.8.5-1.fc38.x86_64
        +      libnghttp2-1.52.0-3.fc38.x86_64
        +      python-pip-wheel-22.3.1-4.fc38.noarch
        +      python3-3.11.9-2.fc38.x86_64
        +      python3-libs-3.11.9-2.fc38.x86_64
        +      tpm2-tss-4.0.2-1.fc38.x86_64
        +      vim-data-2:9.1.393-1.fc38.noarch
        +      vim-minimal-2:9.1.393-1.fc38.x86_64
        +    Installed:
        +      libxcrypt-compat-4.4.36-1.fc38.x86_64
        +
        +    Complete!
        +    42 files removed
        +==> aa51aa7: IMAGE /tmp/velocity/build/fedora-38-aa51aa7/aa51aa7.sif (fedora@38) BUILT [0:01:33]
        +
        +==> BUILT: /tmp/velocity/fedora-38__x86_64-fedora.sif
         

        Adding Different Versions

        So now we have a base Fedora image. That’s great but before we move on let’s make some different versions of the image -so that we have more options for building later. Go ahead and copy the fedora/38 directory several times:

        -
        cp -rf fedora/38/ fedora/39
        -cp -rf fedora/38/ fedora/40
        -cp -rf fedora/38/ fedora/41
        -
        -
        -

        For each of the versions you will need to go in and change the tag on the source. For example docker.io/fedora:38 -in fedora/40/template/fedora.vtmp should be changed to docker.io/fedora:40.

        +so that we have more options for building later. Edit the fedora specs.yaml and add some versions.

        -
        VELOCITY_IMAGE_DIR
        -
        .
        -└── fedora
        -    ├── 38
        -        ├── specifications.yaml
        -        └── templates
        -            └── fedora.vtmp
        -    ├── 39
        -        ├── specifications.yaml
        -        └── templates
        -            └── fedora.vtmp
        -    ├── 40
        -        ├── specifications.yaml
        -        └── templates
        -            └── fedora.vtmp
        -    └── 41
        -        ├── specifications.yaml
        -        └── templates
        -            └── fedora.vtmp
        +
        spec.yaml
        +
        versions:
        +  - spec:
        +      - 38
        +      - 39
        +      - 40
        +      - 41
        +    when: distro=fedora
         
        user@hostname:~$ velocity avail
        -==> System: x86_64
        -==> Backend: podman
        -==> Distro: fedora
        -
         ==> fedora
        -        38
        -        39
        -        40
        -        41
        +    38
        +    39
        +    40
        +    41
         

        Specifying Version

        -

        When building an image Velocity will default to the latest image. To specify a version use <image>@=<version> e.g. -fedora@=40. Versions are compared by splitting the version string up based on periods and then comparing the sections -left to right alphanumerically.

        +

        When building an image Velocity will default to the latest image. To specify a version use <image>@<version> e.g. +fedora@40. Versions take the form <major>.<minor>.<patch>-<suffix>. You can also specify greater than, less +than, and in-between via <image>@<version>:, <image>@:<version> and <image>@<version>:<version> respectively.

        Hello World!

        Now let’s get a little more complicated. Let’s create an image that runs a python script which prints hello world. You can give it whatever version you want:

        -
        VELOCITY_IMAGE_DIR
        -
        .
        -├── fedora
        -│    ├── 38
        -│       ├── specifications.yaml
        -│       └── templates
        -│           └── fedora.vtmp
        -│    ├── 39
        -│       ├── specifications.yaml
        -│       └── templates
        -│           └── fedora.vtmp
        -│    ├── 40
        -│       ├── specifications.yaml
        -│       └── templates
        -│           └── fedora.vtmp
        -│    └── 41
        -│        ├── specifications.yaml
        -│        └── templates
        -│            └── fedora.vtmp
        -└── hello-world
        -    └── 1.0
        -        ├── x86_64
        -           └── hello_world.py
        -        ├── specifications.yaml
        -        └── templates
        -            └── fedora.vtmp
        +
        /tmp/velocity/images
        +
        fedora
        +├── specs.yaml
        +└── templates
        +    └── default.vtmp
        +hello-world
        +├── files
        +│   └── hello_world.py
        +├── specs.yaml
        +└── templates
        +    └── default.vtmp
         
        -

        Notice that now there is a new folder called x86_64 with a python file in it.

        +

        Notice that now there is a new folder called files with a python script in it.

        -
        hello-world/1.0/x86_64/hello_world.py
        +
        hello_world.py
        #!/usr/bin/env python3
         
         print("Hello, World!")
        @@ -518,484 +369,226 @@ 

        Hello World! - -
        build_specifications:
        -
        -  x86_64:
        -    podman:
        -      fedora:
        -        dependencies:
        -          - fedora
        -        variables:
        -          fr: fakeroot
        +
        specs.yaml
        +
        versions:
        +  - spec: 1.0
        +dependencies:
        +  - spec: fedora
        +    when: distro=fedora
        +files:
        +  - name: hello_world.py
         
        -
        hello-world/1.0/templates/fedora.vtmp
        +
        default.vtmp
        @from
        -    %(__base__)
        +    {{ __base__ }}
         
         @copy
        -    hello_world.py /hello_world.py
        +    hello_world.py /hello_world
         
        -@entry
        -    /hello_world.py
        +@run
        +    dnf -y install python3
        +    chmod +x /hello_world
         
        -@label
        -    velocity.image.%(__name__)__%(__tag__) %(__hash__)
        +@entry
        +    /hello_world
         
        user@hostname:~$ velocity avail
        -==> System: x86_64
        -==> Backend: podman
        -==> Distro: fedora
        -
         ==> fedora
        -        38
        -        39
        -        40
        -        41
        +    38
        +    39
        +    40
        +    41
         ==> hello-world
        -        1.0
        +    1.0
         
        user@hostname:~$ velocity build hello-world -v
        -==> System: x86_64
        -==> Backend: podman
        -==> Distro: fedora
        -
         ==> Build Order:
        -        fedora@=41
        -        hello-world@=1.0
        -
        -==> wmrxxohy: BUILD fedora@=41 ...
        -==> wmrxxohy: GENERATING SCRIPT ...
        -        SCRIPT: /tmp/velocity/build/wmrxxohy/script
        -        FROM docker.io/fedora:41
        -
        -        RUN dnf -y upgrade
        -
        -        LABEL velocity.config.system="x86_64" \
        -            velocity.config.backend="podman" \
        -            velocity.config.distro="fedora" \
        -            velocity.image.fedora__41="1ad7926d7e542fa521fe4a2eca54aa73caea82958b1f07adf04728b5762063ac"
        -
        -==> wmrxxohy: BUILDING ...
        -        #!/usr/bin/env bash
        -        podman build -f /tmp/velocity/build/wmrxxohy/script -t localhost/wmrxxohy:latest .;
        -        STEP 1/3: FROM docker.io/fedora:41
        -        STEP 2/3: RUN dnf -y upgrade
        -        Fedora rawhide openh264 (From Cisco) - x86_64    59  B/s | 123  B     00:02
        -        Fedora - Rawhide - Developmental packages for t 3.1 MB/s |  20 MB     00:06
        -        Dependencies resolved.
        -        ======================================================================================
        -         Package                             Arch    Version                    Repo      Size
        -        ======================================================================================
        -        Upgrading:
        -         audit-libs                          x86_64  4.0.1-1.fc41               rawhide  126 k
        -         authselect                          x86_64  1.5.0-5.fc41               rawhide  146 k
        -         authselect-libs                     x86_64  1.5.0-5.fc41               rawhide  219 k
        -         crypto-policies                     noarch  20240320-1.git58e3d95.fc41 rawhide   91 k
        -         curl                                x86_64  8.6.0-7.fc41               rawhide  301 k
        -         dnf                                 noarch  4.19.2-1.fc41              rawhide  503 k
        -         dnf-data                            noarch  4.19.2-1.fc41              rawhide   40 k
        -         elfutils-default-yama-scope         noarch  0.191-5.fc41               rawhide   13 k
        -         elfutils-libelf                     x86_64  0.191-5.fc41               rawhide  209 k
        -         elfutils-libs                       x86_64  0.191-5.fc41               rawhide  258 k
        -         expat                               x86_64  2.6.2-1.fc41               rawhide  113 k
        -         fedora-release-common               noarch  41-0.6                     rawhide   21 k
        -         fedora-release-container            noarch  41-0.6                     rawhide   11 k
        -         fedora-release-identity-container   noarch  41-0.6                     rawhide   12 k
        -         glib2                               x86_64  2.80.0-1.fc41              rawhide  3.0 M
        -         glibc                               x86_64  2.39.9000-10.fc41          rawhide  2.2 M
        -         glibc-common                        x86_64  2.39.9000-10.fc41          rawhide  393 k
        -         glibc-minimal-langpack              x86_64  2.39.9000-10.fc41          rawhide  106 k
        -         gmp                                 x86_64  1:6.3.0-1.fc41             rawhide  317 k
        -         gnupg2                              x86_64  2.4.5-1.fc41               rawhide  2.7 M
        -         gnutls                              x86_64  3.8.4-1.fc41               rawhide  1.1 M
        -         libassuan                           x86_64  2.5.7-1.fc41               rawhide   67 k
        -         libblkid                            x86_64  2.40-0.12.fc41             rawhide  125 k
        -         libcurl                             x86_64  8.6.0-7.fc41               rawhide  345 k
        -         libdnf                              x86_64  0.73.1-1.fc41              rawhide  697 k
        -         libeconf                            x86_64  0.6.2-1.fc41               rawhide   32 k
        -         libffi                              x86_64  3.4.6-1.fc41               rawhide   40 k
        -         libgcc                              x86_64  14.0.1-0.13.fc41           rawhide  123 k
        -         libgcrypt                           x86_64  1.10.3-4.fc41              rawhide  504 k
        -         libgomp                             x86_64  14.0.1-0.13.fc41           rawhide  344 k
        -         libgpg-error                        x86_64  1.48-1.fc41                rawhide  232 k
        -         libksba                             x86_64  1.6.6-1.fc41               rawhide  159 k
        -         libmodulemd                         x86_64  2.15.0-9.fc41              rawhide  233 k
        -         libmount                            x86_64  2.40-0.12.fc41             rawhide  155 k
        -         libnghttp2                          x86_64  1.60.0-2.fc41              rawhide   76 k
        -         librepo                             x86_64  1.17.1-1.fc41              rawhide   99 k
        -         libreport-filesystem                noarch  2.17.15-1.fc41             rawhide   14 k
        -         libsmartcols                        x86_64  2.40-0.12.fc41             rawhide   84 k
        -         libssh                              x86_64  0.10.6-6.fc41              rawhide  212 k
        -         libssh-config                       noarch  0.10.6-6.fc41              rawhide  9.1 k
        -         libstdc++                           x86_64  14.0.1-0.13.fc41           rawhide  881 k
        -         libtirpc                            x86_64  1.3.4-1.rc3.fc41           rawhide   92 k
        -         libunistring                        x86_64  1.1-7.fc41                 rawhide  545 k
        -         libuuid                             x86_64  2.40-0.12.fc41             rawhide   29 k
        -         libxml2                             x86_64  2.12.6-1.fc41              rawhide  686 k
        -         libzstd                             x86_64  1.5.6-1.fc41               rawhide  309 k
        -         npth                                x86_64  1.7-1.fc41                 rawhide   25 k
        -         openssl-libs                        x86_64  1:3.2.1-3.fc41             rawhide  2.3 M
        -         pcre2                               x86_64  10.43-1.fc41               rawhide  242 k
        -         pcre2-syntax                        noarch  10.43-1.fc41               rawhide  149 k
        -         python-pip-wheel                    noarch  24.0-2.fc41                rawhide  1.5 M
        -         python3                             x86_64  3.12.2-3.fc41              rawhide   27 k
        -         python3-dnf                         noarch  4.19.2-1.fc41              rawhide  594 k
        -         python3-hawkey                      x86_64  0.73.1-1.fc41              rawhide  105 k
        -         python3-libdnf                      x86_64  0.73.1-1.fc41              rawhide  847 k
        -         python3-libs                        x86_64  3.12.2-3.fc41              rawhide  9.1 M
        -         shadow-utils                        x86_64  2:4.15.1-2.fc41            rawhide  1.3 M
        -         sqlite-libs                         x86_64  3.45.2-1.fc41              rawhide  706 k
        -         systemd-libs                        x86_64  255.4-1.fc41               rawhide  708 k
        -         tzdata                              noarch  2024a-4.fc41               rawhide  716 k
        -         util-linux-core                     x86_64  2.40-0.12.fc41             rawhide  537 k
        -         vim-data                            noarch  2:9.1.181-1.fc41           rawhide   23 k
        -         vim-minimal                         x86_64  2:9.1.181-1.fc41           rawhide  807 k
        -         xz-libs                             x86_64  1:5.4.6-3.fc41             rawhide  110 k
        -         yum                                 noarch  4.19.2-1.fc41              rawhide   37 k
        -
        -        Transaction Summary
        -        ======================================================================================
        -        Upgrade  65 Packages
        -
        -        Total download size: 38 M
        -        Downloading Packages:
        -        (1/65): audit-libs-4.0.1-1.fc41.x86_64.rpm      136 kB/s | 126 kB     00:00
        -        (2/65): authselect-1.5.0-5.fc41.x86_64.rpm      153 kB/s | 146 kB     00:00
        -        (3/65): crypto-policies-20240320-1.git58e3d95.f 623 kB/s |  91 kB     00:00
        -        (4/65): authselect-libs-1.5.0-5.fc41.x86_64.rpm 199 kB/s | 219 kB     00:01
        -        [MIRROR] dnf-4.19.2-1.fc41.noarch.rpm: Status code: 404 for http://mirror.math.princeton.edu/pub/fedora/linux/development/rawhide/Everything/x86_64/os/Packages/d/dnf-4.19.2-1.fc41.noarch.rpm (IP: 128.112.18.21)
        -        [MIRROR] dnf-data-4.19.2-1.fc41.noarch.rpm: Status code: 404 for http://mirror.math.princeton.edu/pub/fedora/linux/development/rawhide/Everything/x86_64/os/Packages/d/dnf-data-4.19.2-1.fc41.noarch.rpm (IP: 128.112.18.21)
        -        (5/65): curl-8.6.0-7.fc41.x86_64.rpm            919 kB/s | 301 kB     00:00
        -        (6/65): elfutils-default-yama-scope-0.191-5.fc4 105 kB/s |  13 kB     00:00
        -        (7/65): elfutils-libelf-0.191-5.fc41.x86_64.rpm 739 kB/s | 209 kB     00:00
        -        (8/65): dnf-data-4.19.2-1.fc41.noarch.rpm        51 kB/s |  40 kB     00:00
        -        (9/65): elfutils-libs-0.191-5.fc41.x86_64.rpm   980 kB/s | 258 kB     00:00
        -        (10/65): expat-2.6.2-1.fc41.x86_64.rpm          787 kB/s | 113 kB     00:00
        -        (11/65): fedora-release-common-41-0.6.noarch.rp 165 kB/s |  21 kB     00:00
        -        (12/65): fedora-release-container-41-0.6.noarch  87 kB/s |  11 kB     00:00
        -        (13/65): fedora-release-identity-container-41-0  77 kB/s |  12 kB     00:00
        -        (14/65): dnf-4.19.2-1.fc41.noarch.rpm           402 kB/s | 503 kB     00:01
        -        (15/65): glib2-2.80.0-1.fc41.x86_64.rpm         4.8 MB/s | 3.0 MB     00:00
        -        (16/65): glibc-minimal-langpack-2.39.9000-10.fc 783 kB/s | 106 kB     00:00
        -        (17/65): gmp-6.3.0-1.fc41.x86_64.rpm            1.7 MB/s | 317 kB     00:00
        -        (18/65): glibc-common-2.39.9000-10.fc41.x86_64. 507 kB/s | 393 kB     00:00
        -        (19/65): gnupg2-2.4.5-1.fc41.x86_64.rpm         8.2 MB/s | 2.7 MB     00:00
        -        (20/65): libassuan-2.5.7-1.fc41.x86_64.rpm      458 kB/s |  67 kB     00:00
        -        (21/65): libblkid-2.40-0.12.fc41.x86_64.rpm     687 kB/s | 125 kB     00:00
        -        (22/65): glibc-2.39.9000-10.fc41.x86_64.rpm     1.0 MB/s | 2.2 MB     00:02
        -        [MIRROR] libdnf-0.73.1-1.fc41.x86_64.rpm: Status code: 404 for http://mirror.math.princeton.edu/pub/fedora/linux/development/rawhide/Everything/x86_64/os/Packages/l/libdnf-0.73.1-1.fc41.x86_64.rpm (IP: 128.112.18.21)
        -        (23/65): gnutls-3.8.4-1.fc41.x86_64.rpm         668 kB/s | 1.1 MB     00:01
        -        (24/65): libeconf-0.6.2-1.fc41.x86_64.rpm       249 kB/s |  32 kB     00:00
        -        (25/65): libdnf-0.73.1-1.fc41.x86_64.rpm        1.6 MB/s | 697 kB     00:00
        -        (26/65): libffi-3.4.6-1.fc41.x86_64.rpm         205 kB/s |  40 kB     00:00
        -        (27/65): libgcc-14.0.1-0.13.fc41.x86_64.rpm     391 kB/s | 123 kB     00:00
        -        (28/65): libgomp-14.0.1-0.13.fc41.x86_64.rpm    642 kB/s | 344 kB     00:00
        -        (29/65): libgcrypt-1.10.3-4.fc41.x86_64.rpm     613 kB/s | 504 kB     00:00
        -        (30/65): libgpg-error-1.48-1.fc41.x86_64.rpm    579 kB/s | 232 kB     00:00
        -        (31/65): libksba-1.6.6-1.fc41.x86_64.rpm        580 kB/s | 159 kB     00:00
        -        (32/65): libcurl-8.6.0-7.fc41.x86_64.rpm        140 kB/s | 345 kB     00:02
        -        (33/65): libmodulemd-2.15.0-9.fc41.x86_64.rpm   406 kB/s | 233 kB     00:00
        -        (34/65): libmount-2.40-0.12.fc41.x86_64.rpm     271 kB/s | 155 kB     00:00
        -        (35/65): libreport-filesystem-2.17.15-1.fc41.no  82 kB/s |  14 kB     00:00
        -        (36/65): librepo-1.17.1-1.fc41.x86_64.rpm       501 kB/s |  99 kB     00:00
        -        (37/65): libsmartcols-2.40-0.12.fc41.x86_64.rpm 323 kB/s |  84 kB     00:00
        -        (38/65): libssh-0.10.6-6.fc41.x86_64.rpm        744 kB/s | 212 kB     00:00
        -        (39/65): libssh-config-0.10.6-6.fc41.noarch.rpm  74 kB/s | 9.1 kB     00:00
        -        (40/65): libtirpc-1.3.4-1.rc3.fc41.x86_64.rpm   357 kB/s |  92 kB     00:00
        -        (41/65): libstdc++-14.0.1-0.13.fc41.x86_64.rpm  837 kB/s | 881 kB     00:01
        -        (42/65): libnghttp2-1.60.0-2.fc41.x86_64.rpm     35 kB/s |  76 kB     00:02
        -        (43/65): libuuid-2.40-0.12.fc41.x86_64.rpm      208 kB/s |  29 kB     00:00
        -        (44/65): libunistring-1.1-7.fc41.x86_64.rpm     534 kB/s | 545 kB     00:01
        -        (45/65): npth-1.7-1.fc41.x86_64.rpm             187 kB/s |  25 kB     00:00
        -        (46/65): libzstd-1.5.6-1.fc41.x86_64.rpm        767 kB/s | 309 kB     00:00
        -        (47/65): pcre2-10.43-1.fc41.x86_64.rpm          611 kB/s | 242 kB     00:00
        -        (48/65): pcre2-syntax-10.43-1.fc41.noarch.rpm   386 kB/s | 149 kB     00:00
        -        (49/65): python-pip-wheel-24.0-2.fc41.noarch.rp 640 kB/s | 1.5 MB     00:02
        -        (50/65): python3-3.12.2-3.fc41.x86_64.rpm       203 kB/s |  27 kB     00:00
        -        [MIRROR] python3-dnf-4.19.2-1.fc41.noarch.rpm: Status code: 404 for http://mirror.math.princeton.edu/pub/fedora/linux/development/rawhide/Everything/x86_64/os/Packages/p/python3-dnf-4.19.2-1.fc41.noarch.rpm (IP: 128.112.18.21)
        -        (51/65): libxml2-2.12.6-1.fc41.x86_64.rpm       168 kB/s | 686 kB     00:04
        -        (52/65): openssl-libs-3.2.1-3.fc41.x86_64.rpm   554 kB/s | 2.3 MB     00:04
        -        (53/65): python3-hawkey-0.73.1-1.fc41.x86_64.rp 130 kB/s | 105 kB     00:00
        -        (54/65): python3-dnf-4.19.2-1.fc41.noarch.rpm   425 kB/s | 594 kB     00:01
        -        (55/65): shadow-utils-4.15.1-2.fc41.x86_64.rpm  3.5 MB/s | 1.3 MB     00:00
        -        (56/65): sqlite-libs-3.45.2-1.fc41.x86_64.rpm   2.8 MB/s | 706 kB     00:00
        -        (57/65): python3-libdnf-0.73.1-1.fc41.x86_64.rp 678 kB/s | 847 kB     00:01
        -        (58/65): systemd-libs-255.4-1.fc41.x86_64.rpm   2.9 MB/s | 708 kB     00:00
        -        (59/65): util-linux-core-2.40-0.12.fc41.x86_64. 3.0 MB/s | 537 kB     00:00
        -        (60/65): tzdata-2024a-4.fc41.noarch.rpm         1.5 MB/s | 716 kB     00:00
        -        (61/65): vim-data-9.1.181-1.fc41.noarch.rpm     125 kB/s |  23 kB     00:00
        -        (62/65): xz-libs-5.4.6-3.fc41.x86_64.rpm        530 kB/s | 110 kB     00:00
        -        (63/65): yum-4.19.2-1.fc41.noarch.rpm           215 kB/s |  37 kB     00:00
        -        (64/65): python3-libs-3.12.2-3.fc41.x86_64.rpm  3.5 MB/s | 9.1 MB     00:02
        -        (65/65): vim-minimal-9.1.181-1.fc41.x86_64.rpm  218 kB/s | 807 kB     00:03
        -        --------------------------------------------------------------------------------
        -        Total                                           2.0 MB/s |  38 MB     00:19
        -        Running transaction check
        -        Transaction check succeeded.
        -        Running transaction test
        -        Transaction test succeeded.
        -        Running transaction
        -          Preparing        :                                                        1/1
        -          Upgrading        : libgcc-14.0.1-0.13.fc41.x86_64                       1/130
        -          Running scriptlet: libgcc-14.0.1-0.13.fc41.x86_64                       1/130
        -          Upgrading        : tzdata-2024a-4.fc41.noarch                           2/130
        -          Upgrading        : crypto-policies-20240320-1.git58e3d95.fc41.noarc     3/130
        -          Running scriptlet: crypto-policies-20240320-1.git58e3d95.fc41.noarc     3/130
        -          Upgrading        : glibc-common-2.39.9000-10.fc41.x86_64                4/130
        -          Upgrading        : glibc-minimal-langpack-2.39.9000-10.fc41.x86_64      5/130
        -          Running scriptlet: glibc-2.39.9000-10.fc41.x86_64                       6/130
        -          Upgrading        : glibc-2.39.9000-10.fc41.x86_64                       6/130
        -          Running scriptlet: glibc-2.39.9000-10.fc41.x86_64                       6/130
        -          Upgrading        : libgpg-error-1.48-1.fc41.x86_64                      7/130
        -          Upgrading        : libuuid-2.40-0.12.fc41.x86_64                        8/130
        -          Upgrading        : openssl-libs-1:3.2.1-3.fc41.x86_64                   9/130
        -          Upgrading        : xz-libs-1:5.4.6-3.fc41.x86_64                       10/130
        -          Upgrading        : libsmartcols-2.40-0.12.fc41.x86_64                  11/130
        -          Upgrading        : libstdc++-14.0.1-0.13.fc41.x86_64                   12/130
        -          Upgrading        : libzstd-1.5.6-1.fc41.x86_64                         13/130
        -          Upgrading        : sqlite-libs-3.45.2-1.fc41.x86_64                    14/130
        -          Upgrading        : libblkid-2.40-0.12.fc41.x86_64                      15/130
        -          Upgrading        : libmount-2.40-0.12.fc41.x86_64                      16/130
        -          Upgrading        : libffi-3.4.6-1.fc41.x86_64                          17/130
        -          Upgrading        : fedora-release-identity-container-41-0.6.noarch     18/130
        -          Upgrading        : fedora-release-container-41-0.6.noarch              19/130
        -          Upgrading        : fedora-release-common-41-0.6.noarch                 20/130
        -          Upgrading        : elfutils-libelf-0.191-5.fc41.x86_64                 21/130
        -          Upgrading        : systemd-libs-255.4-1.fc41.x86_64                    22/130
        -          Upgrading        : libxml2-2.12.6-1.fc41.x86_64                        23/130
        -          Upgrading        : libassuan-2.5.7-1.fc41.x86_64                       24/130
        -          Upgrading        : libgcrypt-1.10.3-4.fc41.x86_64                      25/130
        -          Upgrading        : libksba-1.6.6-1.fc41.x86_64                         26/130
        -          Upgrading        : audit-libs-4.0.1-1.fc41.x86_64                      27/130
        -          Upgrading        : authselect-libs-1.5.0-5.fc41.x86_64                 28/130
        -          Upgrading        : expat-2.6.2-1.fc41.x86_64                           29/130
        -          Upgrading        : gmp-1:6.3.0-1.fc41.x86_64                           30/130
        -          Upgrading        : libeconf-0.6.2-1.fc41.x86_64                        31/130
        -          Upgrading        : libnghttp2-1.60.0-2.fc41.x86_64                     32/130
        -          Upgrading        : libtirpc-1.3.4-1.rc3.fc41.x86_64                    33/130
        -          Upgrading        : libunistring-1.1-7.fc41.x86_64                      34/130
        -          Upgrading        : gnutls-3.8.4-1.fc41.x86_64                          35/130
        -          Upgrading        : npth-1.7-1.fc41.x86_64                              36/130
        -          Upgrading        : vim-data-2:9.1.181-1.fc41.noarch                    37/130
        -          Upgrading        : python-pip-wheel-24.0-2.fc41.noarch                 38/130
        -          Upgrading        : python3-3.12.2-3.fc41.x86_64                        39/130
        -          Upgrading        : python3-libs-3.12.2-3.fc41.x86_64                   40/130
        -          Upgrading        : pcre2-syntax-10.43-1.fc41.noarch                    41/130
        -          Upgrading        : pcre2-10.43-1.fc41.x86_64                           42/130
        -          Upgrading        : glib2-2.80.0-1.fc41.x86_64                          43/130
        -          Upgrading        : libmodulemd-2.15.0-9.fc41.x86_64                    44/130
        -          Upgrading        : libssh-config-0.10.6-6.fc41.noarch                  45/130
        -          Upgrading        : libssh-0.10.6-6.fc41.x86_64                         46/130
        -          Upgrading        : libcurl-8.6.0-7.fc41.x86_64                         47/130
        -          Upgrading        : librepo-1.17.1-1.fc41.x86_64                        48/130
        -          Upgrading        : libdnf-0.73.1-1.fc41.x86_64                         49/130
        -          Upgrading        : python3-libdnf-0.73.1-1.fc41.x86_64                 50/130
        -          Upgrading        : python3-hawkey-0.73.1-1.fc41.x86_64                 51/130
        -          Upgrading        : libreport-filesystem-2.17.15-1.fc41.noarch          52/130
        -          Upgrading        : dnf-data-4.19.2-1.fc41.noarch                       53/130
        -          Upgrading        : python3-dnf-4.19.2-1.fc41.noarch                    54/130
        -          Upgrading        : dnf-4.19.2-1.fc41.noarch                            55/130
        -          Running scriptlet: dnf-4.19.2-1.fc41.noarch                            55/130
        -          Upgrading        : elfutils-default-yama-scope-0.191-5.fc41.noarch     56/130
        -          Running scriptlet: elfutils-default-yama-scope-0.191-5.fc41.noarch     56/130
        -          Upgrading        : elfutils-libs-0.191-5.fc41.x86_64                   57/130
        -          Upgrading        : yum-4.19.2-1.fc41.noarch                            58/130
        -          Upgrading        : curl-8.6.0-7.fc41.x86_64                            59/130
        -          Upgrading        : vim-minimal-2:9.1.181-1.fc41.x86_64                 60/130
        -          Upgrading        : gnupg2-2.4.5-1.fc41.x86_64                          61/130
        -          Upgrading        : shadow-utils-2:4.15.1-2.fc41.x86_64                 62/130
        -          Upgrading        : authselect-1.5.0-5.fc41.x86_64                      63/130
        -          Upgrading        : util-linux-core-2.40-0.12.fc41.x86_64               64/130
        -          Upgrading        : libgomp-14.0.1-0.13.fc41.x86_64                     65/130
        -          Cleanup          : util-linux-core-2.40-0.9.rc1.fc41.x86_64            66/130
        -          Cleanup          : systemd-libs-255.3-1.fc40.x86_64                    67/130
        -          Cleanup          : gnupg2-2.4.4-1.fc40.x86_64                          68/130
        -          Cleanup          : elfutils-libs-0.190-6.fc40.x86_64                   69/130
        -          Cleanup          : shadow-utils-2:4.14.0-6.fc40.x86_64                 70/130
        -          Cleanup          : vim-minimal-2:9.1.113-1.fc41.x86_64                 71/130
        -          Running scriptlet: authselect-1.5.0-3.fc40.x86_64                      72/130
        -          Cleanup          : authselect-1.5.0-3.fc40.x86_64                      72/130
        -          Cleanup          : curl-8.6.0-6.fc40.x86_64                            73/130
        -          Cleanup          : fedora-release-common-41-0.1.noarch                 74/130
        -          Cleanup          : libgcrypt-1.10.3-3.fc40.x86_64                      75/130
        -          Cleanup          : elfutils-libelf-0.190-6.fc40.x86_64                 76/130
        -          Cleanup          : libassuan-2.5.6-4.fc40.x86_64                       77/130
        -          Cleanup          : authselect-libs-1.5.0-3.fc40.x86_64                 78/130
        -          Cleanup          : audit-libs-4.0-8.fc40.x86_64                        79/130
        -          Cleanup          : libksba-1.6.5-3.fc40.x86_64                         80/130
        -          Cleanup          : libgpg-error-1.47-4.fc40.x86_64                     81/130
        -          Cleanup          : libeconf-0.5.2-3.fc40.x86_64                        82/130
        -          Cleanup          : libgomp-14.0.1-0.6.fc40.x86_64                      83/130
        -          Cleanup          : fedora-release-container-41-0.1.noarch              84/130
        -          Cleanup          : yum-4.19.0-1.fc40.noarch                            85/130
        -          Cleanup          : libzstd-1.5.5-5.fc40.x86_64                         86/130
        -          Cleanup          : npth-1.6-18.fc40.x86_64                             87/130
        -          Running scriptlet: dnf-4.19.0-1.fc40.noarch                            88/130
        -          Cleanup          : dnf-4.19.0-1.fc40.noarch                            88/130
        -          Running scriptlet: dnf-4.19.0-1.fc40.noarch                            88/130
        -          Cleanup          : python3-dnf-4.19.0-1.fc40.noarch                    89/130
        -          Cleanup          : dnf-data-4.19.0-1.fc40.noarch                       90/130
        -          Cleanup          : libreport-filesystem-2.17.14-1.fc40.noarch          91/130
        -          Cleanup          : fedora-release-identity-container-41-0.1.noarch     92/130
        -          Cleanup          : vim-data-2:9.1.113-1.fc41.noarch                    93/130
        -          Cleanup          : elfutils-default-yama-scope-0.190-6.fc40.noarch     94/130
        -          Cleanup          : python3-hawkey-0.73.0-1.fc40.x86_64                 95/130
        -          Cleanup          : python3-libdnf-0.73.0-1.fc40.x86_64                 96/130
        -          Cleanup          : python3-libs-3.12.2-1.fc40.x86_64                   97/130
        -          Cleanup          : python3-3.12.2-1.fc40.x86_64                        98/130
        -          Cleanup          : libdnf-0.73.0-1.fc40.x86_64                         99/130
        -          Cleanup          : python-pip-wheel-23.3.2-1.fc40.noarch              100/130
        -          Cleanup          : libstdc++-14.0.1-0.6.fc40.x86_64                   101/130
        -          Cleanup          : librepo-1.17.0-3.fc40.x86_64                       102/130
        -          Cleanup          : libcurl-8.6.0-6.fc40.x86_64                        103/130
        -          Cleanup          : libxml2-2.12.5-1.fc40.x86_64                       104/130
        -          Cleanup          : libssh-0.10.6-4.fc40.x86_64                        105/130
        -          Cleanup          : openssl-libs-1:3.2.1-2.fc40.x86_64                 106/130
        -          Cleanup          : sqlite-libs-3.45.1-2.fc40.x86_64                   107/130
        -          Cleanup          : libtirpc-1.3.4-1.rc2.fc40.2.x86_64                 108/130
        -          Cleanup          : libmodulemd-2.15.0-8.fc40.x86_64                   109/130
        -          Cleanup          : glib2-2.79.1-1.fc40.x86_64                         110/130
        -          Cleanup          : libmount-2.40-0.9.rc1.fc41.x86_64                  111/130
        -          Cleanup          : gnutls-3.8.3-2.fc40.x86_64                         112/130
        -          Cleanup          : libblkid-2.40-0.9.rc1.fc41.x86_64                  113/130
        -          Cleanup          : libuuid-2.40-0.9.rc1.fc41.x86_64                   114/130
        -          Cleanup          : xz-libs-5.4.6-1.fc40.x86_64                        115/130
        -          Cleanup          : libsmartcols-2.40-0.9.rc1.fc41.x86_64              116/130
        -          Cleanup          : expat-2.6.0-1.fc41.x86_64                          117/130
        -          Cleanup          : gmp-1:6.2.1-8.fc40.x86_64                          118/130
        -          Cleanup          : libunistring-1.1-7.fc40.x86_64                     119/130
        -          Cleanup          : libffi-3.4.4-7.fc40.x86_64                         120/130
        -          Cleanup          : pcre2-10.42-2.fc40.2.x86_64                        121/130
        -          Cleanup          : libnghttp2-1.59.0-2.fc40.x86_64                    122/130
        -          Cleanup          : pcre2-syntax-10.42-2.fc40.2.noarch                 123/130
        -          Cleanup          : crypto-policies-20240201-1.git9f501f3.fc40.noarc   124/130
        -          Cleanup          : libssh-config-0.10.6-4.fc40.noarch                 125/130
        -          Cleanup          : glibc-2.39.9000-1.fc41.x86_64                      126/130
        -          Cleanup          : glibc-minimal-langpack-2.39.9000-1.fc41.x86_64     127/130
        -          Cleanup          : glibc-common-2.39.9000-1.fc41.x86_64               128/130
        -          Cleanup          : tzdata-2024a-2.fc40.noarch                         129/130
        -          Cleanup          : libgcc-14.0.1-0.6.fc40.x86_64                      130/130
        -          Running scriptlet: libgcc-14.0.1-0.6.fc40.x86_64                      130/130
        -          Running scriptlet: authselect-libs-1.5.0-5.fc41.x86_64                130/130
        -          Running scriptlet: libgcc-14.0.1-0.6.fc40.x86_64                      130/130
        -
        -        Upgraded:
        -          audit-libs-4.0.1-1.fc41.x86_64
        -          authselect-1.5.0-5.fc41.x86_64
        -          authselect-libs-1.5.0-5.fc41.x86_64
        -          crypto-policies-20240320-1.git58e3d95.fc41.noarch
        -          curl-8.6.0-7.fc41.x86_64
        -          dnf-4.19.2-1.fc41.noarch
        -          dnf-data-4.19.2-1.fc41.noarch
        -          elfutils-default-yama-scope-0.191-5.fc41.noarch
        -          elfutils-libelf-0.191-5.fc41.x86_64
        -          elfutils-libs-0.191-5.fc41.x86_64
        -          expat-2.6.2-1.fc41.x86_64
        -          fedora-release-common-41-0.6.noarch
        -          fedora-release-container-41-0.6.noarch
        -          fedora-release-identity-container-41-0.6.noarch
        -          glib2-2.80.0-1.fc41.x86_64
        -          glibc-2.39.9000-10.fc41.x86_64
        -          glibc-common-2.39.9000-10.fc41.x86_64
        -          glibc-minimal-langpack-2.39.9000-10.fc41.x86_64
        -          gmp-1:6.3.0-1.fc41.x86_64
        -          gnupg2-2.4.5-1.fc41.x86_64
        -          gnutls-3.8.4-1.fc41.x86_64
        -          libassuan-2.5.7-1.fc41.x86_64
        -          libblkid-2.40-0.12.fc41.x86_64
        -          libcurl-8.6.0-7.fc41.x86_64
        -          libdnf-0.73.1-1.fc41.x86_64
        -          libeconf-0.6.2-1.fc41.x86_64
        -          libffi-3.4.6-1.fc41.x86_64
        -          libgcc-14.0.1-0.13.fc41.x86_64
        -          libgcrypt-1.10.3-4.fc41.x86_64
        -          libgomp-14.0.1-0.13.fc41.x86_64
        -          libgpg-error-1.48-1.fc41.x86_64
        -          libksba-1.6.6-1.fc41.x86_64
        -          libmodulemd-2.15.0-9.fc41.x86_64
        -          libmount-2.40-0.12.fc41.x86_64
        -          libnghttp2-1.60.0-2.fc41.x86_64
        -          librepo-1.17.1-1.fc41.x86_64
        -          libreport-filesystem-2.17.15-1.fc41.noarch
        -          libsmartcols-2.40-0.12.fc41.x86_64
        -          libssh-0.10.6-6.fc41.x86_64
        -          libssh-config-0.10.6-6.fc41.noarch
        -          libstdc++-14.0.1-0.13.fc41.x86_64
        -          libtirpc-1.3.4-1.rc3.fc41.x86_64
        -          libunistring-1.1-7.fc41.x86_64
        -          libuuid-2.40-0.12.fc41.x86_64
        -          libxml2-2.12.6-1.fc41.x86_64
        -          libzstd-1.5.6-1.fc41.x86_64
        -          npth-1.7-1.fc41.x86_64
        -          openssl-libs-1:3.2.1-3.fc41.x86_64
        -          pcre2-10.43-1.fc41.x86_64
        -          pcre2-syntax-10.43-1.fc41.noarch
        -          python-pip-wheel-24.0-2.fc41.noarch
        -          python3-3.12.2-3.fc41.x86_64
        -          python3-dnf-4.19.2-1.fc41.noarch
        -          python3-hawkey-0.73.1-1.fc41.x86_64
        -          python3-libdnf-0.73.1-1.fc41.x86_64
        -          python3-libs-3.12.2-3.fc41.x86_64
        -          shadow-utils-2:4.15.1-2.fc41.x86_64
        -          sqlite-libs-3.45.2-1.fc41.x86_64
        -          systemd-libs-255.4-1.fc41.x86_64
        -          tzdata-2024a-4.fc41.noarch
        -          util-linux-core-2.40-0.12.fc41.x86_64
        -          vim-data-2:9.1.181-1.fc41.noarch
        -          vim-minimal-2:9.1.181-1.fc41.x86_64
        -          xz-libs-1:5.4.6-3.fc41.x86_64
        -          yum-4.19.2-1.fc41.noarch
        -
        -        Complete!
        -        --> 43089713ea9
        -        STEP 3/3: LABEL velocity.config.system="x86_64"     velocity.config.backend="podman"     velocity.config.distro="fedora"     velocity.image.fedora__41="1ad7926d7e542fa521fe4a2eca54aa73caea82958b1f07adf04728b5762063ac"
        -        COMMIT localhost/wmrxxohy:latest
        -        --> 49321c240b5
        -        Successfully tagged localhost/wmrxxohy:latest
        -        49321c240b522a0d66cd30b62addc99e87b5861e2e6c27f6d0136968c73be5aa
        -==> wmrxxohy: IMAGE localhost/wmrxxohy:latest (fedora@=41) BUILT [0:01:02]
        -
        -==> bdvdbcor: BUILD hello-world@=1.0 ...
        -==> bdvdbcor: COPYING FILES ...
        -        FILE: /home/xjv/tmp/hello-world/1.0/x86_64/hello_world.py -> /tmp/velocity/build/bdvdbcor/hello_world.py
        -==> bdvdbcor: GENERATING SCRIPT ...
        -        SCRIPT: /tmp/velocity/build/bdvdbcor/script
        -        FROM localhost/wmrxxohy:latest
        -
        -        COPY hello_world.py /hello_world.py
        -
        -        LABEL velocity.image.hello-world__1.0="1b080f458c34ae2759d1eb3e8464d3d508e3bcb981a476f70709b0f20b6218bb"
        -
        -        ENTRYPOINT ['/hello_world.py']
        -
        -==> bdvdbcor: BUILDING ...
        -        #!/usr/bin/env bash
        -        podman build -f /tmp/velocity/build/bdvdbcor/script -t localhost/hello-world__1.0__x86_64__fedora:latest .;
        -        STEP 1/4: FROM localhost/wmrxxohy:latest
        -        STEP 2/4: COPY hello_world.py /hello_world.py
        -        --> fa1896e8689
        -        STEP 3/4: LABEL velocity.image.hello-world__1.0="1b080f458c34ae2759d1eb3e8464d3d508e3bcb981a476f70709b0f20b6218bb"
        -        --> d58637575df
        -        STEP 4/4: ENTRYPOINT ['/hello_world.py']
        -        COMMIT localhost/hello-world__1.0__x86_64__fedora:latest
        -        --> 21211d9de40
        -        Successfully tagged localhost/hello-world__1.0__x86_64__fedora:latest
        -        21211d9de40aa6c0cb6b625e6f4fed265c88f9a4be09c4edcd93a78360580ecf
        -==> bdvdbcor: IMAGE localhost/hello-world__1.0__x86_64__fedora:latest (hello-world@=1.0) BUILT [0:00:03]
        +    fedora@41-8a9a360
        +    hello-world@1.0-de9c02b
        +
        +==> 8a9a360: BUILD fedora@41 ...
        +==> 8a9a360: GENERATING SCRIPT ...
        +    SCRIPT: /tmp/velocity/build/fedora-41-8a9a360/script
        +    Bootstrap: docker
        +    From: docker.io/fedora:41
        +
        +    %post
        +    dnf -y upgrade
        +    dnf clean all
        +==> 8a9a360: BUILDING ...
        +    #!/usr/bin/env bash
        +    apptainer build --disable-cache /tmp/velocity/build/fedora-41-8a9a360/8a9a360.sif /tmp/velocity/build/fedora-41-8a9a360/script;
        +    Updating and loading repositories:
        +     Fedora 41 openh264 (From Cisco) - x86_ 100% |   6.5 KiB/s |   4.8 KiB |  00m01s
        +     Fedora 41 - x86_64 - Test Updates      100% |   1.8 MiB/s |   3.4 MiB |  00m02s
        +     Fedora 41 - x86_64                     100% |  13.9 MiB/s |  35.9 MiB |  00m03s
        +     Fedora 41 - x86_64 - Updates           100% |  50.3 KiB/s |  31.9 KiB |  00m01s
        +    Repositories loaded.
        +    Package                     Arch   Version        Repository                            Size
        +    Upgrading:
        +     libgcc                     x86_64 14.2.1-3.fc41  updates-testing                  274.6 KiB
        +       replacing libgcc         x86_64 14.2.1-1.fc41  34086d2996104518800c8d7dcc6139a1 274.6 KiB
        +     libgomp                    x86_64 14.2.1-3.fc41  updates-testing                  523.5 KiB
        +       replacing libgomp        x86_64 14.2.1-1.fc41  34086d2996104518800c8d7dcc6139a1 523.4 KiB
        +     libstdc++                  x86_64 14.2.1-3.fc41  updates-testing                    2.8 MiB
        +       replacing libstdc++      x86_64 14.2.1-1.fc41  34086d2996104518800c8d7dcc6139a1   2.8 MiB
        +     openssl-libs               x86_64 1:3.2.2-7.fc41 updates-testing                    7.8 MiB
        +       replacing openssl-libs   x86_64 1:3.2.2-5.fc41 34086d2996104518800c8d7dcc6139a1   7.8 MiB
        +     rpm                        x86_64 4.19.94-1.fc41 updates-testing                    3.1 MiB
        +       replacing rpm            x86_64 4.19.92-6.fc41 34086d2996104518800c8d7dcc6139a1   3.1 MiB
        +     rpm-build-libs             x86_64 4.19.94-1.fc41 updates-testing                  206.7 KiB
        +       replacing rpm-build-libs x86_64 4.19.92-6.fc41 34086d2996104518800c8d7dcc6139a1 206.7 KiB
        +     rpm-libs                   x86_64 4.19.94-1.fc41 updates-testing                  721.9 KiB
        +       replacing rpm-libs       x86_64 4.19.92-6.fc41 34086d2996104518800c8d7dcc6139a1 721.9 KiB
        +     systemd-libs               x86_64 256.6-1.fc41   updates-testing                    2.0 MiB
        +       replacing systemd-libs   x86_64 256.5-1.fc41   34086d2996104518800c8d7dcc6139a1   2.0 MiB
        +     zlib-ng-compat             x86_64 2.1.7-3.fc41   updates-testing                  134.0 KiB
        +       replacing zlib-ng-compat x86_64 2.1.7-2.fc41   34086d2996104518800c8d7dcc6139a1 134.0 KiB
        +
        +    Transaction Summary:
        +     Upgrading:         9 packages
        +     Replacing:         9 packages
        +
        +    Total size of inbound packages is 5 MiB. Need to download 5 MiB.
        +    After this operation 2 KiB will be used (install 17 MiB, remove 17 MiB).
        +    [1/9] libgcc-0:14.2.1-3.fc41.x86_64     100% | 132.2 KiB/s | 133.3 KiB |  00m01s
        +    [2/9] libstdc++-0:14.2.1-3.fc41.x86_64  100% | 583.3 KiB/s | 887.8 KiB |  00m02s
        +    [3/9] libgomp-0:14.2.1-3.fc41.x86_64    100% | 208.5 KiB/s | 354.1 KiB |  00m02s
        +    [4/9] rpm-0:4.19.94-1.fc41.x86_64       100% |   1.6 MiB/s | 547.6 KiB |  00m00s
        +    [5/9] rpm-build-libs-0:4.19.94-1.fc41.x 100% |   2.1 MiB/s |  99.1 KiB |  00m00s
        +    [6/9] openssl-libs-1:3.2.2-7.fc41.x86_6 100% |   2.6 MiB/s |   2.3 MiB |  00m01s
        +    [7/9] rpm-libs-0:4.19.94-1.fc41.x86_64  100% |   1.1 MiB/s | 309.5 KiB |  00m00s
        +    [8/9] zlib-ng-compat-0:2.1.7-3.fc41.x86 100% |   1.0 MiB/s |  77.7 KiB |  00m00s
        +    [9/9] systemd-libs-0:256.6-1.fc41.x86_6 100% |   2.6 MiB/s | 730.9 KiB |  00m00s
        +    --------------------------------------------------------------------------------
        +    [9/9] Total                             100% |   2.2 MiB/s |   5.4 MiB |  00m02s
        +    Running transaction
        +    [ 1/20] Verify package files            100% | 750.0   B/s |   9.0   B |  00m00s
        +    [ 2/20] Prepare transaction             100% |   1.6 KiB/s |  18.0   B |  00m00s
        +    [ 3/20] Upgrading libgcc-0:14.2.1-3.fc4 100% |  14.2 MiB/s | 276.3 KiB |  00m00s
        +    >>> Running post-install scriptlet: libgcc-0:14.2.1-3.fc41.x86_64
        +    >>> Stop post-install scriptlet: libgcc-0:14.2.1-3.fc41.x86_64
        +    [ 4/20] Upgrading zlib-ng-compat-0:2.1. 100% |  32.9 MiB/s | 134.8 KiB |  00m00s
        +    [ 5/20] Upgrading rpm-libs-0:4.19.94-1. 100% | 117.7 MiB/s | 723.4 KiB |  00m00s
        +    [ 6/20] Upgrading libgomp-0:14.2.1-3.fc 100% | 170.8 MiB/s | 524.8 KiB |  00m00s
        +    [ 7/20] Upgrading rpm-build-libs-0:4.19 100% |  15.6 MiB/s | 207.5 KiB |  00m00s
        +    >>> Running pre-install scriptlet: rpm-0:4.19.94-1.fc41.x86_64
        +    >>> Stop pre-install scriptlet: rpm-0:4.19.94-1.fc41.x86_64
        +    [ 8/20] Upgrading rpm-0:4.19.94-1.fc41. 100% | 104.3 MiB/s |   2.5 MiB |  00m00s
        +    [ 9/20] Upgrading openssl-libs-1:3.2.2- 100% | 190.9 MiB/s |   7.8 MiB |  00m00s
        +    [10/20] Upgrading libstdc++-0:14.2.1-3. 100% | 145.6 MiB/s |   2.8 MiB |  00m00s
        +    [11/20] Upgrading systemd-libs-0:256.6- 100% | 135.3 MiB/s |   2.0 MiB |  00m00s
        +    [12/20] Erasing rpm-build-libs-0:4.19.9 100% |   1.0 KiB/s |   5.0   B |  00m00s
        +    [13/20] Erasing libstdc++-0:14.2.1-1.fc 100% |   6.1 KiB/s |  31.0   B |  00m00s
        +    [14/20] Erasing systemd-libs-0:256.5-1. 100% |   2.4 KiB/s |  20.0   B |  00m00s
        +    [15/20] Erasing rpm-0:4.19.92-6.fc41.x8 100% |  26.7 KiB/s | 273.0   B |  00m00s
        +    [16/20] Erasing rpm-libs-0:4.19.92-6.fc 100% |   2.4 KiB/s |  10.0   B |  00m00s
        +    [17/20] Erasing openssl-libs-1:3.2.2-5. 100% |   9.5 KiB/s |  39.0   B |  00m00s
        +    [18/20] Erasing zlib-ng-compat-0:2.1.7- 100% |   1.2 KiB/s |   5.0   B |  00m00s
        +    [19/20] Erasing libgcc-0:14.2.1-1.fc41. 100% | 647.0   B/s |  11.0   B |  00m00s
        +    >>> Running post-uninstall scriptlet: libgcc-0:14.2.1-1.fc41.x86_64
        +    >>> Stop post-uninstall scriptlet: libgcc-0:14.2.1-1.fc41.x86_64
        +    [20/20] Erasing libgomp-0:14.2.1-1.fc41 100% |  43.0   B/s |   9.0   B |  00m00s
        +    >>> Running post-transaction scriptlet: rpm-0:4.19.94-1.fc41.x86_64
        +    >>> Stop post-transaction scriptlet: rpm-0:4.19.94-1.fc41.x86_64
        +    >>> Running trigger-install scriptlet: glibc-common-0:2.40-3.fc41.x86_64
        +    >>> Stop trigger-install scriptlet: glibc-common-0:2.40-3.fc41.x86_64
        +    >>> Running trigger-post-uninstall scriptlet: glibc-common-0:2.40-3.fc41.x86_64
        +    >>> Stop trigger-post-uninstall scriptlet: glibc-common-0:2.40-3.fc41.x86_64
        +    Complete!
        +    Removed 24 files, 13 directories. 0 errors occurred.
        +==> 8a9a360: IMAGE /tmp/velocity/build/fedora-41-8a9a360/8a9a360.sif (fedora@41) BUILT [0:00:21]
        +
        +==> de9c02b: BUILD hello-world@1.0 ...
        +==> de9c02b: COPYING FILES ...
        +    FILE: /tmp/velocity/images/hello-world/files/hello_world.py -> /tmp/velocity/build/hello-world-1.0-de9c02b/hello_world.py
        +==> de9c02b: GENERATING SCRIPT ...
        +    SCRIPT: /tmp/velocity/build/hello-world-1.0-de9c02b/script
        +    Bootstrap: localimage
        +    From: /tmp/velocity/build/fedora-41-8a9a360/8a9a360.sif
        +
        +    %files
        +    hello_world.py /hello_world
        +
        +    %post
        +    dnf -y install python3
        +    chmod +x /hello_world
        +
        +    %runscript
        +    /hello_world
        +==> de9c02b: BUILDING ...
        +    #!/usr/bin/env bash
        +    apptainer build --disable-cache /tmp/velocity/build/hello-world-1.0-de9c02b/de9c02b.sif /tmp/velocity/build/hello-world-1.0-de9c02b/script;
        +    Updating and loading repositories:
        +     Fedora 41 openh264 (From Cisco) - x86_ 100% |  11.1 KiB/s |   6.0 KiB |  00m01s
        +     Fedora 41 - x86_64                     100% |  11.7 MiB/s |  35.4 MiB |  00m03s
        +     Fedora 41 - x86_64 - Updates           100% |  50.1 KiB/s |  31.9 KiB |  00m01s
        +     Fedora 41 - x86_64 - Test Updates      100% |   1.8 MiB/s |   2.1 MiB |  00m01s
        +    Repositories loaded.
        +    Package                     Arch   Version           Repository           Size
        +    Installing:
        +     python3                    x86_64 3.13.0~rc2-1.fc41 fedora           31.8 KiB
        +    Installing dependencies:
        +     expat                      x86_64 2.6.3-1.fc41      updates-testing 291.5 KiB
        +     libb2                      x86_64 0.98.1-12.fc41    fedora           42.2 KiB
        +     mpdecimal                  x86_64 2.5.1-16.fc41     fedora          204.9 KiB
        +     python-pip-wheel           noarch 24.2-1.fc41       fedora            1.2 MiB
        +     python3-libs               x86_64 3.13.0~rc2-1.fc41 fedora           40.3 MiB
        +    Installing weak dependencies:
        +     python-unversioned-command noarch 3.13.0~rc2-1.fc41 fedora           23.0   B
        +
        +    Transaction Summary:
        +     Installing:        7 packages
        +
        +    Total size of inbound packages is 11 MiB. Need to download 11 MiB.
        +    After this operation 42 MiB will be used (install 42 MiB, remove 0 B).
        +    [1/7] libb2-0:0.98.1-12.fc41.x86_64     100% |  94.1 KiB/s |  25.7 KiB |  00m00s
        +    [2/7] python3-0:3.13.0~rc2-1.fc41.x86_6 100% |  95.9 KiB/s |  27.4 KiB |  00m00s
        +    [3/7] mpdecimal-0:2.5.1-16.fc41.x86_64  100% | 408.0 KiB/s |  89.0 KiB |  00m00s
        +    [4/7] expat-0:2.6.3-1.fc41.x86_64       100% | 447.4 KiB/s | 114.1 KiB |  00m00s
        +    [5/7] python-pip-wheel-0:24.2-1.fc41.no 100% |   2.5 MiB/s |   1.2 MiB |  00m00s
        +    [6/7] python-unversioned-command-0:3.13 100% | 125.1 KiB/s |  10.5 KiB |  00m00s
        +    [7/7] python3-libs-0:3.13.0~rc2-1.fc41. 100% |   8.1 MiB/s |   9.1 MiB |  00m01s
        +    --------------------------------------------------------------------------------
        +    [7/7] Total                             100% |   6.7 MiB/s |  10.6 MiB |  00m02s
        +    Running transaction
        +    [1/9] Verify package files              100% | 269.0   B/s |   7.0   B |  00m00s
        +    [2/9] Prepare transaction               100% | 280.0   B/s |   7.0   B |  00m00s
        +    [3/9] Installing expat-0:2.6.3-1.fc41.x 100% |  71.7 MiB/s | 293.6 KiB |  00m00s
        +    [4/9] Installing python-pip-wheel-0:24. 100% | 310.4 MiB/s |   1.2 MiB |  00m00s
        +    [5/9] Installing mpdecimal-0:2.5.1-16.f 100% |  50.3 MiB/s | 206.0 KiB |  00m00s
        +    [6/9] Installing libb2-0:0.98.1-12.fc41 100% |   4.7 MiB/s |  43.3 KiB |  00m00s
        +    [7/9] Installing python3-libs-0:3.13.0~ 100% | 139.9 MiB/s |  40.7 MiB |  00m00s
        +    [8/9] Installing python3-0:3.13.0~rc2-1 100% |  10.9 MiB/s |  33.6 KiB |  00m00s
        +    [9/9] Installing python-unversioned-com 100% |   1.8 KiB/s | 424.0   B |  00m00s
        +    >>> Running trigger-install scriptlet: glibc-common-0:2.40-3.fc41.x86_64
        +    >>> Stop trigger-install scriptlet: glibc-common-0:2.40-3.fc41.x86_64
        +    Complete!
        +==> de9c02b: IMAGE /tmp/velocity/build/hello-world-1.0-de9c02b/de9c02b.sif (hello-world@1.0) BUILT [0:00:14]
        +
        +==> BUILT: /tmp/velocity/hello-world-1.0_fedora-41__x86_64-fedora.sif
         

        Our hello-world image has been built!

        -
        user@hostname:~$ podman image ls
        -REPOSITORY                                 TAG         IMAGE ID      CREATED        SIZE
        -localhost/hello-world__1.0__x86_64__fedora latest      db958bad4f40  4 minutes ago  337 MB
        -docker.io/library/fedora                   41          54d1373b70a2  5 weeks ago    180 MB
        +
        user@hostname:~$ ls
        +total 190972
        +drwxr-xr-x  4 xjv  users      4096 Sep 18 10:01 .
        +drwxrwxrwt 31 root root     131072 Sep 18 10:01 ..
        +drwxr-xr-x  4 xjv  users      4096 Sep 18 10:01 build
        +-rwxr-xr-x  1 xjv  users  66007040 Sep 18 09:42 fedora-38__x86_64-fedora.sif
        +-rwxr-xr-x  1 xjv  users 129392640 Sep 18 10:01 hello-world-1.0_fedora-41__x86_64-fedora.sif
        +drwxr-xr-x  4 xjv  users      4096 Sep 18 09:44 images
        +
        +
        +

        Now you can run the image!

        +
        user@hostname:~$ apptainer run hello-world-1.0_fedora-41__x86_64-fedora.sif
        +Hello, World!
         

        diff --git a/docs/source/_static/icon.png b/docs/source/_static/icon.png index 1f964a2..2c31d5a 120000 --- a/docs/source/_static/icon.png +++ b/docs/source/_static/icon.png @@ -1 +1 @@ -../../../assets/artwork/icon.drawio.png \ No newline at end of file +../../../misc/artwork/icon.drawio.png \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index e81dc9e..fcb521a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -4,8 +4,10 @@ contain the root `toctree` directive. -Velocity Container Build System -=============================== +Velocity Container Build Management +=================================== +Velocity is a tool to help with the maintenance of container build scripts on multiple systems, backends +(e.g podman or apptainer) and distros. .. toctree:: :maxdepth: 2 diff --git a/docs/source/reference/build.rst b/docs/source/reference/build.rst index 4d4d7f1..6003d21 100644 --- a/docs/source/reference/build.rst +++ b/docs/source/reference/build.rst @@ -6,16 +6,14 @@ This page describes the steps that velocity goes through when building an image. Setup ##### -After selecting images to build and ordering them. Velocity will start by assigning a random -id to each image to be built. It will also create a directory BSD (Build Sub Dir) named with that random id in -`VELOCITY_BUILD_DIR`. +After selecting images to build and ordering them Velocity will also create a directory BSD (Build Sub Dir) +named ``--`` in build directory. .. _files: Files ##### -Next Velocity will look for a folder in the image definition directory with the same name as `VELOCITY_SYSTEM`. All -of the files in this directory will be copied to the BSD. +Next Velocity will copy any files that you specified. Parse Template ############## @@ -32,18 +30,20 @@ Finally velocity will run the `build` script and log the output to a file called Build Directory Layout ###################### -At the end of a build `VELOCITY_BUILD_DIR` should look somthing like this. With the addition of any other files that +At the end of a build the build dir should look somthing like this. With the addition of any other files that got copied in the :ref:`files` step. .. code-block:: bash . - ├── jknqsnkc - │ ├── build - │ ├── log - │ └── script - └── nfhmhmsh + ├── fedora-41-8a9a360 + │ ├── 8a9a360.sif + │ ├── build + │ ├── log + │ └── script + └── hello-world-1.0-de9c02b ├── build + ├── de9c02b.sif + ├── hello_world.py ├── log └── script - diff --git a/docs/source/reference/config.rst b/docs/source/reference/config.rst new file mode 100644 index 0000000..50a83c3 --- /dev/null +++ b/docs/source/reference/config.rst @@ -0,0 +1,64 @@ +************* +Configuration +************* + +Configuration for Velocity comes from three places. Command line options, environment variables and the configuration file. + +Commandline Options +################### +These take the highest level of precedence and can be viewed by using the ``--help`` option in the command line. + +Variables +######### +Variables are the second highest level of configuration. + +`VELOCITY_IMAGE_PATH` +--------------------- + +This variable points to the directories containing the the image definitions. + +`VELOCITY_SYSTEM` +----------------- +This variable specifies what computer system you are building for (e.g. frontier). + +`VELOCITY_BACKEND` +------------------ +This variable specifies the container backend that should be used (e.g podman). + +`VELOCITY_DISTRO` +----------------- +This variable specifies the distro of the container images that will be built. This name is flexable and completely +up to the user. It is used purely for organizational purposes. + +`VELOCITY_BUILD_DIR` +-------------------- +This variable specifies a scratch space for Velocity to preform builds in. + +`VELOCITY_CONFIG_DIR` +--------------------- +This variable specifies where to look for the configuration file. + +Configuration File +################## +The configuration file is the lowest level of configuration. By default Velocity looks for ``config.yaml`` in +``~/.velocity`` unless ``VELOCITY_CONFIG_DIR`` is set. A number of configuration option for velocity can be set. + +.. code-block:: yaml + + velocity: + system: frontier + backend: apptainer + distro: ubuntu + debug: INFO # set the debug level + image_path: # a list of : seperated paths + build_dir: # path to a scratch space + +Additionally you can set :ref:`arguments` and :ref:`specVariables` at a global level in the constraints section. As an example here +we are adding ``--disable-cache`` as an argument for every image build we do with apptainer. + +.. code-block:: yaml + + constraints: + arguments: + - value: --disable-cache + when: backend=apptainer diff --git a/docs/source/reference/index.rst b/docs/source/reference/index.rst index f386b53..0c01598 100644 --- a/docs/source/reference/index.rst +++ b/docs/source/reference/index.rst @@ -8,6 +8,7 @@ Technical Docs .. toctree:: :maxdepth: 2 + config vtmp - specifications + specs build diff --git a/docs/source/reference/specifications.rst b/docs/source/reference/specifications.rst deleted file mode 100644 index b02e926..0000000 --- a/docs/source/reference/specifications.rst +++ /dev/null @@ -1,160 +0,0 @@ -******************* -Specifications.yaml -******************* - -Basic Layout -############ -The `specifications.yaml` file defines image availability, dependencies and a number of other available -options for an image. Velocity doesn't care about any extraneous data you may have in it. All that Velocity looks -for is a base level section called `build_specifications`. Any other sections that you may add for your own purposes -are ignored by Velocity. Under `build_specifications` there should be a tree defining under what conditions the image -can be built in the order `system` > `backend` > `distro`. For example if I am creating an `centos` based image for use on -frontier which only has Apptainer on it then I should have the following structure in my `specifications.yaml` file. - -.. code-block:: yaml - - build_specifications: - frontier: - apptainer: - ubuntu: - # config options - -If I wanted it to be available on summit as well which has Apptainer and Podman I would add the following. - -.. code-block:: yaml - - build_specifications: - frontier: - apptainer: - centos: - # config options - summit: - apptainer: - centos: - # config options - podman: - centos: - # config options - -While `system` and `backend` are fairly self explanatory the `distro` is a little more obscure. Velocity does not do -any checks to make sure that the image being built is actually of the right distro. All that the distro tells Velocity to do is -look in the `templates` folder of an image for a template called `.vtmp` (e.g. centos.vtmp). If a template with -the right name cannot be found Velocity will error. - -.. code-block:: bash - :caption: image structure - - . - └── - └── - ├── specifications.yaml - └── templates - └── .vtmp - - -Config Options -############## -Under each `system` > `backend` > `distro` section there are a variaty of config options available. - -dependencies ------------- -The `dependencies` section is a list of images that must be built before the current one can be. - -.. code-block:: yaml - - build_specifications: - frontier: - apptainer: - ubuntu: - dependencies: - - dep_one - - dep_two - # ... - -Some times you may want a specific version of a dependency in which case Velocity provides several ways to specify dependency -relationships. Versions are compared by splitting the version string up based on periods and then comparing the sections -left to right alphanumerically. - -.. code-block:: yaml - - build_specifications: - frontier: - apptainer: - ubuntu: - dependencies: - - dep_one@=11.5 # equal to 11.5 - - dep_two@_11.5 # less than or equal to 11.5 - - dep_three@^11.5 # greater than or equal to 11.5 - - dep_four@11.2%11.8 # inbetween 11.2 and 11.8 (inclusive) - # ... - -prolog ------- -There are certain occasions when some commands need to be run on the host machine before an image is built. These -commands can be placed in a bash script in a directory which matches the system name. All that you need to do in the -`specifications.yaml` file is put the script name. - -.. code-block:: yaml - - build_specifications: - frontier: - apptainer: - ubuntu: - dependencies: - - dep_one - - dep_two - # ... - prolog: example_prolog.sh - -.. code-block:: bash - :caption: VELOCITY_IMAGE_DIR - - . - ├── - └── - ├── - │ └── example_prolog.sh - ├── specifications.yaml - └── templates - └── .vtmp - -arguments ---------- -Any arguments specified here will be added to the build command. - -.. code-block:: yaml - - build_specifications: - frontier: - apptainer: - ubuntu: - dependencies: - - dep_one - - dep_two - # ... - prolog: example_prolog.sh - arguments: - - '--build-arg EXAMPLE=8046' - -This will result in `apptainer build --build-arg EXAMPLE=8046 ...` when the image is built. - -variables ---------- -Here you can define :ref:`variables` for the VTMP file to use when it is parsed. - -.. code-block:: yaml - - build_specifications: - frontier: - apptainer: - ubuntu: - dependencies: - - dep_one - - dep_two - # ... - prolog: example_prolog.sh - arguments: - - '--build-arg EXAMPLE=8046' - variables: - - arch: x86_64 - - url: 'https://example.com' diff --git a/docs/source/reference/specs.rst b/docs/source/reference/specs.rst new file mode 100644 index 0000000..d3667f4 --- /dev/null +++ b/docs/source/reference/specs.rst @@ -0,0 +1,114 @@ +******************* +Specs.yaml +******************* + +About +##### +The `specs.yaml` file defines image availability, dependencies and a number of other available +options for an image. + +Config Options +############## + +versions +-------- +The `versions` section defines a list of versions and their availability. + +.. code-block:: yaml + + versions: + - spec: 45 + when: distro=ubuntu + - spec: + - 46 + - 47 + when: backend=podman + +dependencies +------------ +The `dependencies` section is used to specify prerequist images. + +.. code-block:: yaml + + dependencies: + - spec: ubuntu + when: distro=ubuntu + - spec: + - "gcc@12:" + - pytorch + when: myapp@:2.3 + +templates +--------- +By default velocity looks for a template called ``default.vtmp``, but using the `templates` section you can specify an +alternate template. + +.. code-block:: yaml + + templates: + - name: second_tmp + when: "myapp@2:" + +.. _arguments: + +arguments +--------- +Use the `argument` section to add build time arguments to an image. + +.. code-block:: yaml + + arguments: + - value: --disable-cache + when: backend=apptainer + +.. _specVariables: + +variables +--------- +Use the `variables` section to set template variables. + +.. code-block:: yaml + + variables: + - name: test_var + value: "Test value." + +files +----- +Use the `files` section to specify files or directories in the `files` folder that you want copied to the build directory. + +.. code-block:: yaml + + files: + - name: config.json + +prologs +------- +Use the `prologs` section to bash commands that you want to run before the build. + +.. code-block:: yaml + + prologs: + - script: | + git clone ... + when: system=summit + +Using ``when`` +############## +A few notes about using ``when`` to filter config options. The ``when`` option can be used to filter configs as shown +above by system, backend, distro and dependencies. The only exception to this is the `versions` section which cannot +be filtered by dependencies. List each item that you want to filter by separated by a space e.g. ``gcc@12.3 system=frontier``. +Additionally you can specify the scope of a ``when`` by specifying the ``scope``. The default scope is ``image`` which means +that the when statement is evaluated on the current image. So say you want to apply a config in the gcc `specs.yaml` file +to every gcc version greater than 10.3.0, you would use ``when: ^ubuntu:``. Alternatively you can set the scope to ``build`` +when you want the when statement evaluated on the current build. + +.. code-block:: yaml + + variable: + - name: TEST + value: "1 2 3 4" + when: ubuntu + scope: build + +This can be read as "If the ubuntu image is in the current set of images to be built then add the TEST variable." diff --git a/docs/source/reference/vtmp.rst b/docs/source/reference/vtmp.rst index fe621ed..2c05c5a 100644 --- a/docs/source/reference/vtmp.rst +++ b/docs/source/reference/vtmp.rst @@ -3,76 +3,94 @@ VTMP Format *********** VTMP files are used by Velocity to generate the appropriate script for a particular backend. -While certain features are supported across all backends, others are exclusive to one backend and many are interpreted -quite differently between the different backends. The goal of the VTMP files are to provide a unified API for +The goal of the VTMP files are to provide a unified API for container images across multiple backends but they are not feature exhaustive. VTMP files are split up into a number of sections. .. code-block:: text :caption: Example VTMP + >>> this is a comment @from - # can only take one of these + >>> can only take one of these examples docker.io/ubuntu:20.04 - /example.sif # only works for apptainer backend - %(__base__) + /example.sif >>> only works for apptainer backend + {{ __base__ }} >>> build on the previous image - @pre # same as post but placed right after @from - # anything here is taken literally - # denote the start of a line with | - |echo 'Without tab' + @pre + >>> anything here is taken literally + >>> optionally denote the start of a line with | (helpful for white space) + echo 'Without tab' | echo 'With tab' - @arg - # define arg names - example_arg # reference this argument throughout the script with @() - @copy - # list files to copy, format + >>> list files to copy, format /external/file /internal/file @run - # list of commands to run in container + >>> list of commands to run in container echo 'Hello world' - ?podman echo 'podman' - ?apptainer echo 'apptainer' - echo %(template_variable) - echo @(example_arg) # see @arg + ?? distro=podman |> echo 'podman' ?? >>> conditional + ?? distro=apptainer |> echo 'apptainer' ?? + echo {{ template_variable }} + echo @@ example_arg @@ >>> define an argument + !envar TEST_VAR I want this here and in the @env section >>> the !envar directive @env - # set env variables + >>> set env variables PATH /something:$PATH @label - # set container labels - velocity.image.%(__name__)__%(__tag__) %(__hash__) - + >>> set container labels + IMAGE_NAME {{ __name__ }} @entry - # set entrypoint or %runscript - # only one line allowed + >>> set ENTRYPOINT or %runscript + >>> only one line allowed + /bin/bash - @post # same as pre but placed after everything else + @post >>> same as pre but placed after everything else .. _variables: Variables ######### The first thing that is done to a VTMP file is the substitution of any variables found in the script. Variables are indicated -by `%()`. In Velocity variables can be defined by the user in the `specifications.yaml` file, but Velocity also +by ``{{ }}``. In Velocity variables can be defined by the user in the `specs.yaml` file, but Velocity also provides a set of build time variables. .. code-block:: text __backend__ __distro__ - __hash__ # template hash - __base__ # previous image to build on - __name__ # current image name e.g. cuda + __base__ >>> previous image to build on + __name__ >>> current image name e.g. cuda __system__ - __tag__ # image "version" e.g. 11.7.1 + __version__ >>> image "version" e.g. 11.7.1 + __version_major__ + __version_minor__ + __version_patch__ + __version_suffix__ __timestamp__ + >>> the versions of all images in a build are also available in the following format + ____version__ + ____version_major__ + ____version_minor__ + ____version_patch__ + ____version_suffix__ + +Arguments +######### +You can also pass in arguments at build time using the form ``@@ @@``, but don't forget to add the +argument in ``specs.yaml`` so that is gets added to the build command or the build will fail. + +Conditionals +############ +If there is a line in a template that you only want under certain conditions use ``?? |> ??``. +The whole conditional will be replaced with the section if the condition is true; otherwise, it will substitute +an empty string. The conditionals can include the following components, ``system=``, ``backend=``, +``distro=`` and a dependency in the form ``^``. Sections ######## @@ -92,12 +110,6 @@ allows you to add white space to the beginning of a line. The only difference be are where they are placed in the script the `@pre` section is placed at the beginning of the script right after the `@from` section. -@arg ----- -The `@arg` section specifies build time arguments that are passed to the backend. If these arguments are not passed to -the backend at build time it will fail. Once an arg has been defined in the `@arg` section it can be referenced -throughout the script with `@()`. - @copy ----- The `@copy` section takes a list of files/dir to be copied in the format. @@ -105,12 +117,14 @@ The `@copy` section takes a list of files/dir to be copied in the f @run ---- The `@run` section takes a list of commands to be run in the container. These commands should be written as if they all -occur one after the other in the same shell. +occur one after the other in the same shell. One added feature to the @run section is the ``!envar`` directive. This +directive will add the following variable definition to the @env section as well as to the current @run section. Use +the format ``!envar ``. @env ---- The `@env` section sets environment variables. These variables will only be available when the container is run or in -the next build so if there is a variable that is needed in the run section you must declare it there as well. +the next build so if there is a variable that is needed in the run section use the ``!envar`` directive in the @run section. @label ------ @@ -125,8 +139,3 @@ in the `@entry` section. @post ----- See :ref:`@pre `. The `@post` section is placed at the end of the script after all other sections. - -Conditionals -############ -It is handy somtimes to be able to limit certain commands to a backend(s) this can be done by placing a `?` -at the beginning of the line in question. diff --git a/docs/source/starting/basic.rst b/docs/source/starting/basic.rst index 8c4abb5..99d4beb 100644 --- a/docs/source/starting/basic.rst +++ b/docs/source/starting/basic.rst @@ -3,131 +3,82 @@ Overview ******** -Concept -####### -Velocity is a tool to help with the maintenance of a variety of container builds on multiple systems, backends -(e.g podman or apptainer) and distros. - How it Works ############ Velocity works by building a set of containers in a chain so that the final container has all of the needed components. - - -Layout -###### -The contents of the Velocity repo are fairly simple. - -.. code-block:: - - . - ├── docs - ├── lib - ├── README.md - ├── setup-env.sh - └── velocity - -For informational purposes there is a folder `docs` which holds this documentation and a README file. -The `setup-env.sh` script can be used to 'install' velocity. The lib folder holds a python package which the -velocity script needs to run. - - - +Velocity maintains a very hands off approach. It is only as good as the templates/configuration that you write. +In general it will assume that a particular build will work unless you tell it otherwise. It is important to note +that while Velocity has many features that are similar to those provided by a package manager, it is NOT a +package manager. Rather it should be viewed as a templating and build orchestration tool. Installation ############ -First you will need to set up a conda environment for Velocity and install the following packages: - -.. note:: - - For more info on creating a custom conda environment on OLCF systems visit https://docs.olcf.ornl.gov/software/python/index.html#custom-environments. - -.. code-block:: - - conda install pyyaml networkx colorama python-editor - -.. important:: - - If you wish to build the docs you will also need to install `sphinx` and `sphinx-rtd-theme`. - -Next clone the Velocity git repository to the desired location. +The easiest way to install velocity is to install prebuilt python packages using pip. .. code-block:: bash - git clone https://gitlab.ccs.ornl.gov/saue-software/velocity.git + pip install olcf-velocity -You can then setup Velocity by sourcing the `setup-env.sh` script. +You can also clone the velocity repository and build/install velocity from source. .. code-block:: bash - . ./velocity/setup-env.sh - -The `setup-env.sh` script will help you choose the value of several environment :ref:`variables` -that velocity uses. + git clone https://github.com/olcf/velocity.git + cd velocity + python3 -m build + # install the built python wheel package + pip install dist/olcf-velocity-*.whl -You should now be able to run the `velocity` command. +Now you can use Velocity as a python module! We recommend setting a bash alias for convenience. .. code-block:: bash + user@hostname:~$ alias velocity="python3 -m velocity" user@hostname:~$ velocity - ==> System: x86_64 - ==> Backend: podman - ==> Distro: fedora + usage: velocity [-h] [-v] [-D {TRACE,DEBUG,INFO,SUCCESS,WARNING,ERROR,CRITICAL}] [-b BACKEND] [-s SYSTEM] [-d DISTRO] {build,avail,spec} ... - usage: velocity [-h] [-v] [-b BACKEND] [-s SYSTEM] [-d DISTRO] - {build,avail,spec,edit} ... - - Build tool for OLCF containers + build tool for OLCF containers positional arguments: - {build,avail,spec,edit} + {build,avail,spec} build build specified container image avail lookup available images spec lookup image dependencies - edit edit image files options: -h, --help show this help message and exit -v, --version program version + -D {TRACE,DEBUG,INFO,SUCCESS,WARNING,ERROR,CRITICAL}, --debug {TRACE,DEBUG,INFO,SUCCESS,WARNING,ERROR,CRITICAL} + set debug output level -b BACKEND, --backend BACKEND -s SYSTEM, --system SYSTEM -d DISTRO, --distro DISTRO - See (https://gitlab.ccs.ornl.gov/saue-software/velocity) - + See https://github.com/olcf/velocity .. _configuration: Configuration ############# -There are five system variables that need to be set for Velocity to work (these are set in the `setup-env.sh` script). +Velocity has a number of configuration options. The basic ones are setting the system name, container backend, +container distro, the path(s) to your image definitions and the build scratch directory. +To see more configuration options go to the :doc:`configuration ` page. The easiest way to configure velocity is to +edit ``~/.velocity/config.yaml``. -`VELOCITY_IMAGE_DIR` --------------------- -This variable points to the directory containing the the image definitions. +.. code-block:: + + velocity: + system: frontier + backend: apptainer + distro: ubuntu + image_path: # a list of : seperated paths + build_dir: # path to a scratch space .. note:: Image definitions can be created by the user as needed but a base set for usage at OLCF are provided at - `https://gitlab.ccs.ornl.gov/saue-software/velocity-images.git` - -`VELOCITY_SYSTEM` ------------------ -This variable specifies what computer system you are building for (e.g. frontier). - -`VELOCITY_BACKEND` ------------------- -This variable specifies the container backend that should be used (e.g podman). - -`VELOCITY_DISTRO` ------------------ -This variable specifies the distro of the container images that will be built. - -`VELOCITY_BUILD_DIR` --------------------- -This variable specifies a scratch space for Velocity to preform builds in. - - + https://github.com/olcf/velocity-images Basic Usage ########### @@ -140,27 +91,20 @@ The `avail` command prints the defined images that can be built. .. code-block:: bash user@hostname:~$ velocity avail - ==> System: x86_64 - ==> Backend: podman - ==> Distro: centos - - ==> centos - stream8 ==> gcc - 11.2.0 - ==> hmmer - 3.4 - ==> kalign - 3.4.0 - ==> miniforge3 - 23.11.0 - ==> python - 3.11.8 - ==> pytorch - latest - -Each image is listed and then indented underneath is a list of the available versions -(in velocity they are called tags). + 12.3.0 + 13.2.0 + 14.1.0 + ==> mpich + 3.4.3 + ==> rocm + 5.7.1 + ==> ubuntu + 20.04 + 22.04 + 24.04 + +Each image is listed and then indented underneath is a list of the available versions. `spec` ------ @@ -170,37 +114,28 @@ The `spec` command shows the dependencies for a given image (or list of images) .. code-block:: bash user@hostname:~$ velocity spec pytorch - ==> System: summit - ==> Backend: podman - ==> Distro: centos - - > pytorch@=latest - ^cuda@=11.7.1 - ^centos@=stream8 - ^cudnn@=8.5.0.96 - ^cuda@=11.7.1 - ^centos@=stream8 - ^spectrum-mpi@=10.4.0.6 - ^centos@=stream8 - ^gcc@=11.2.0 - ^centos@=stream8 - ^miniforge3@=23.11.0 - ^centos@=stream8 - + > pytorch@latest + ^cuda@11.7.1 + ^centos@stream8 + ^cudnn@8.5.0.96 + ^cuda@11.7.1 + ^centos@stream8 + ^spectrum-mpi@10.4.0.6 + ^centos@stream8 + ^gcc@11.2.0 + ^centos@stream8 + ^miniforge3@23.11.0 + ^centos@stream8 `build` ------- -The `build` can be used to build an container image from one or more image definitions. +The `build` command can be used to build an container image from one or more image definitions. .. code-block:: bash user@hostname:~$ velocity build centos - ==> System: x86_64 - ==> Backend: podman - ==> Distro: centos - ==> Build Order: centos@=stream8 @@ -214,10 +149,6 @@ Both the spec and the build command can also take a list of images. .. code-block:: bash user@hostname:~$ velocity build gcc python - ==> System: x86_64 - ==> Backend: podman - ==> Distro: centos - ==> Build Order: centos@=stream8 gcc@=11.2.0 @@ -237,20 +168,3 @@ Both the spec and the build command can also take a list of images. ==> sunflyhd: GENERATING SCRIPT ... ==> sunflyhd: BUILDING ... ==> sunflyhd: IMAGE localhost/python__3.11.8__x86_64__centos:latest (python@=3.11.8) BUILT [0:23:19] - -`edit` ------- -The edit command can be used to edit the VTMP or specification file for an image. By default -it edits the VTMP file. Add `-s` to edit the `specifications.yaml` file. - -.. code-block:: bash - - user@hostname:~$ velocity edit --help - usage: velocity edit [-h] [-s] target - - positional arguments: - target image to edit - - options: - -h, --help show this help message and exit - -s, --specification edit the specifications file diff --git a/docs/source/starting/tutorial.rst b/docs/source/starting/tutorial.rst index 2214604..d232464 100644 --- a/docs/source/starting/tutorial.rst +++ b/docs/source/starting/tutorial.rst @@ -2,395 +2,261 @@ Creating Your First Image ************************* -Prerequisites -############# -You will need to have the Velocity repo cloned and the following environment variables set. You can do this by -sourcing the `setup-env.sh` script. - -.. code-block:: bash - - VELOCITY_IMAGE_DIR= - VELOCITY_BACKEND=podman # if you use apptainer you will need to make changes to some of the examples - VELOCITY_BUILD_DIR=/tmp/velocity/build - VELOCITY_SYSTEM=x86_64 # if you use a different system name you will need to make changes to some of the examples - VELOCITY_ROOT= # you will also need to add this to PATH - VELOCITY_DISTRO=fedora - Base Image ########## -Let's start with a simple base image. This image will pull an fedora docker image and update the packages. -Start by creating a directory in the image directory called `fedora` (for this tutorial we are starting with an empty -image directory). Next we need to create a directory in the `fedora` directory for the version of fedora that we want. -Let's use `38`. In this directory create a file called `specifications.yaml` and a directory called `templates` with -a file named `fedora.vtmp`. Your image directory and files should now look like this. +Let's start with a simple base image. This image will pull an fedora docker image and update the packages. For this +tutorial I have created the empty directory ``/tmp/velocity/images`` and configured it as the image directory. I have set +the build directory to ``/tmp/velocity/build``, the backend to ``apptainer`` and the distro as ``fedora``. All +commands are run in ``/tmp/velocity``. +Start by creating a directory in the image directory called ``fedora``. In this directory create a file called +``specs.yaml`` and a directory called ``templates`` with +a file named ``default.vtmp``. Your image directory and files should now look like this. .. code-block:: bash - :caption: VELOCITY_IMAGE_DIR + :caption: /tmp/velocity/images + + fedora + ├── specs.yaml + └── templates + └── default.vtmp - . - └── fedora - └── 38 - ├── specifications.yaml - └── templates - └── fedora.vtmp .. code-block:: yaml - :caption: fedora/38/specifications.yaml + :caption: specs.yaml - build_specifications: + versions: + - spec: 38 + when: distro=fedora - x86_64: - podman: - fedora: {} .. code-block:: text - :caption: fedora/38/templates/fedora.vtmp + :caption: default.vtmp @from - docker.io/fedora:38 + docker.io/fedora:{{ __version__ }} @run dnf -y upgrade - - @label - velocity.config.system %(__system__) - velocity.config.backend %(__backend__) - velocity.config.distro %(__distro__) - velocity.image.%(__name__)__%(__tag__) %(__hash__) + dnf clean all Now if you run `velocity avail` you should get the following. .. code-block:: bash user@hostname:~$ velocity avail - ==> System: x86_64 - ==> Backend: podman - ==> Distro: fedora - ==> fedora - 38 + 38 Now build the image. .. code-block:: bash user@hostname:~$ velocity build fedora - ==> System: x86_64 - ==> Backend: podman - ==> Distro: fedora - ==> Build Order: - fedora@=38 + fedora@38-aa51aa7 + + ==> aa51aa7: BUILD fedora@38 ... + ==> aa51aa7: GENERATING SCRIPT ... + ==> aa51aa7: BUILDING ... + ==> aa51aa7: IMAGE /tmp/velocity/build/fedora-38-aa51aa7/aa51aa7.sif (fedora@38) BUILT [0:01:07] - ==> yftozouc: BUILD fedora@=38 ... - ==> yftozouc: GENERATING SCRIPT ... - ==> yftozouc: BUILDING ... - ==> yftozouc: IMAGE localhost/fedora__38__x86_64__fedora:latest (fedora@=38) BUILT [0:07:10] + ==> BUILT: /tmp/velocity/fedora-38__x86_64-fedora.sif If you wish to see more output you can add the `-v` flag: .. code-block:: bash user@hostname:~$ velocity build fedora -v - ==> System: x86_64 - ==> Backend: podman - ==> Distro: fedora - ==> Build Order: - fedora@=38 - - ==> xuoykrdt: BUILD fedora@=38 ... - ==> xuoykrdt: GENERATING SCRIPT ... - SCRIPT: /tmp/velocity/build/xuoykrdt/script - FROM docker.io/fedora:38 - - RUN dnf -y upgrade - - LABEL velocity.config.system="x86_64" \ - velocity.config.backend="podman" \ - velocity.config.distro="fedora" \ - velocity.image.fedora__38="ff9fa85cf102560cf3fe2014c3c758fbb3809247537abbeab2c4b67c62dda164" - - ==> xuoykrdt: BUILDING ... - #!/usr/bin/env bash - podman build -f /tmp/velocity/build/xuoykrdt/script -t localhost/fedora__38__x86_64__fedora:latest .; - STEP 1/3: FROM docker.io/fedora:38 - STEP 2/3: RUN dnf -y upgrade - Fedora 38 - x86_64 131 kB/s | 84 MB 10:53 - Fedora 38 openh264 (From Cisco) - x86_64 2.9 kB/s | 2.6 kB 00:00 - Fedora Modular 38 - x86_64 1.1 MB/s | 2.8 MB 00:02 - Fedora 38 - x86_64 - Updates 4.5 MB/s | 40 MB 00:09 - Fedora Modular 38 - x86_64 - Updates 152 kB/s | 2.1 MB 00:14 - Last metadata expiration check: 0:00:01 ago on Wed Mar 27 20:09:54 2024. - Dependencies resolved. - ================================================================================ - Package Arch Version Repo Size - ================================================================================ - Upgrading: - curl x86_64 8.0.1-7.fc38 updates 348 k - dnf noarch 4.19.0-1.fc38 updates 507 k - dnf-data noarch 4.19.0-1.fc38 updates 39 k - elfutils-default-yama-scope noarch 0.191-1.fc38 updates 12 k - elfutils-libelf x86_64 0.191-1.fc38 updates 208 k - elfutils-libs x86_64 0.191-1.fc38 updates 263 k - expat x86_64 2.6.0-1.fc38 updates 112 k - keyutils-libs x86_64 1.6.3-1.fc38 updates 31 k - libcurl x86_64 8.0.1-7.fc38 updates 315 k - libdnf x86_64 0.73.0-1.fc38 updates 681 k - libgcc x86_64 13.2.1-7.fc38 updates 115 k - libgomp x86_64 13.2.1-7.fc38 updates 324 k - libsolv x86_64 0.7.28-1.fc38 updates 426 k - libstdc++ x86_64 13.2.1-7.fc38 updates 870 k - ncurses-base noarch 6.4-7.20230520.fc38.1 updates 88 k - ncurses-libs x86_64 6.4-7.20230520.fc38.1 updates 336 k - python3 x86_64 3.11.8-2.fc38 updates 28 k - python3-dnf noarch 4.19.0-1.fc38 updates 606 k - python3-hawkey x86_64 0.73.0-1.fc38 updates 107 k - python3-libdnf x86_64 0.73.0-1.fc38 updates 859 k - python3-libs x86_64 3.11.8-2.fc38 updates 9.6 M - systemd-libs x86_64 253.17-1.fc38 updates 649 k - vim-data noarch 2:9.1.158-1.fc38 updates 23 k - vim-minimal x86_64 2:9.1.158-1.fc38 updates 808 k - yum noarch 4.19.0-1.fc38 updates 37 k - - Transaction Summary - ================================================================================ - Upgrade 25 Packages - - Total download size: 17 M - Downloading Packages: - (1/25): dnf-data-4.19.0-1.fc38.noarch.rpm 107 kB/s | 39 kB 00:00 - (2/25): elfutils-default-yama-scope-0.191-1.fc3 224 kB/s | 12 kB 00:00 - (3/25): curl-8.0.1-7.fc38.x86_64.rpm 637 kB/s | 348 kB 00:00 - (4/25): dnf-4.19.0-1.fc38.noarch.rpm 807 kB/s | 507 kB 00:00 - (5/25): elfutils-libelf-0.191-1.fc38.x86_64.rpm 901 kB/s | 208 kB 00:00 - (6/25): expat-2.6.0-1.fc38.x86_64.rpm 1.1 MB/s | 112 kB 00:00 - (7/25): keyutils-libs-1.6.3-1.fc38.x86_64.rpm 331 kB/s | 31 kB 00:00 - (8/25): elfutils-libs-0.191-1.fc38.x86_64.rpm 1.1 MB/s | 263 kB 00:00 - (9/25): libgcc-13.2.1-7.fc38.x86_64.rpm 872 kB/s | 115 kB 00:00 - (10/25): libcurl-8.0.1-7.fc38.x86_64.rpm 1.7 MB/s | 315 kB 00:00 - (11/25): libgomp-13.2.1-7.fc38.x86_64.rpm 1.8 MB/s | 324 kB 00:00 - (12/25): libdnf-0.73.0-1.fc38.x86_64.rpm 1.8 MB/s | 681 kB 00:00 - (13/25): libsolv-0.7.28-1.fc38.x86_64.rpm 2.1 MB/s | 426 kB 00:00 - (14/25): ncurses-base-6.4-7.20230520.fc38.1.noa 423 kB/s | 88 kB 00:00 - (15/25): ncurses-libs-6.4-7.20230520.fc38.1.x86 1.3 MB/s | 336 kB 00:00 - (16/25): python3-3.11.8-2.fc38.x86_64.rpm 286 kB/s | 28 kB 00:00 - (17/25): libstdc++-13.2.1-7.fc38.x86_64.rpm 1.8 MB/s | 870 kB 00:00 - (18/25): python3-hawkey-0.73.0-1.fc38.x86_64.rp 834 kB/s | 107 kB 00:00 - (19/25): python3-dnf-4.19.0-1.fc38.noarch.rpm 2.5 MB/s | 606 kB 00:00 - (20/25): python3-libdnf-0.73.0-1.fc38.x86_64.rp 1.6 MB/s | 859 kB 00:00 - (21/25): systemd-libs-253.17-1.fc38.x86_64.rpm 1.4 MB/s | 649 kB 00:00 - (22/25): vim-data-9.1.158-1.fc38.noarch.rpm 196 kB/s | 23 kB 00:00 - (23/25): yum-4.19.0-1.fc38.noarch.rpm 270 kB/s | 37 kB 00:00 - (24/25): vim-minimal-9.1.158-1.fc38.x86_64.rpm 2.5 MB/s | 808 kB 00:00 - (25/25): python3-libs-3.11.8-2.fc38.x86_64.rpm 3.6 MB/s | 9.6 MB 00:02 - -------------------------------------------------------------------------------- - Total 3.6 MB/s | 17 MB 00:04 - Running transaction check - Transaction check succeeded. - Running transaction test - Transaction test succeeded. - Running transaction - Preparing : 1/1 - Upgrading : libgcc-13.2.1-7.fc38.x86_64 1/50 - Running scriptlet: libgcc-13.2.1-7.fc38.x86_64 1/50 - Upgrading : libstdc++-13.2.1-7.fc38.x86_64 2/50 - Upgrading : libsolv-0.7.28-1.fc38.x86_64 3/50 - Upgrading : libdnf-0.73.0-1.fc38.x86_64 4/50 - Upgrading : vim-data-2:9.1.158-1.fc38.noarch 5/50 - Upgrading : ncurses-base-6.4-7.20230520.fc38.1.noarch 6/50 - Upgrading : ncurses-libs-6.4-7.20230520.fc38.1.x86_64 7/50 - Upgrading : libcurl-8.0.1-7.fc38.x86_64 8/50 - Upgrading : expat-2.6.0-1.fc38.x86_64 9/50 - Upgrading : python3-3.11.8-2.fc38.x86_64 10/50 - Upgrading : python3-libs-3.11.8-2.fc38.x86_64 11/50 - Upgrading : python3-libdnf-0.73.0-1.fc38.x86_64 12/50 - Upgrading : python3-hawkey-0.73.0-1.fc38.x86_64 13/50 - Upgrading : elfutils-libelf-0.191-1.fc38.x86_64 14/50 - Upgrading : elfutils-default-yama-scope-0.191-1.fc38.noarch 15/50 - Running scriptlet: elfutils-default-yama-scope-0.191-1.fc38.noarch 15/50 - Upgrading : dnf-data-4.19.0-1.fc38.noarch 16/50 - Upgrading : python3-dnf-4.19.0-1.fc38.noarch 17/50 - Upgrading : dnf-4.19.0-1.fc38.noarch 18/50 - Running scriptlet: dnf-4.19.0-1.fc38.noarch 18/50 - Upgrading : yum-4.19.0-1.fc38.noarch 19/50 - Upgrading : elfutils-libs-0.191-1.fc38.x86_64 20/50 - Upgrading : curl-8.0.1-7.fc38.x86_64 21/50 - Upgrading : vim-minimal-2:9.1.158-1.fc38.x86_64 22/50 - Upgrading : systemd-libs-253.17-1.fc38.x86_64 23/50 - Upgrading : libgomp-13.2.1-7.fc38.x86_64 24/50 - Upgrading : keyutils-libs-1.6.3-1.fc38.x86_64 25/50 - Cleanup : elfutils-libs-0.190-2.fc38.x86_64 26/50 - Cleanup : systemd-libs-253.15-2.fc38.x86_64 27/50 - Cleanup : vim-minimal-2:9.1.113-1.fc38.x86_64 28/50 - Cleanup : curl-8.0.1-6.fc38.x86_64 29/50 - Cleanup : yum-4.18.2-1.fc38.noarch 30/50 - Running scriptlet: dnf-4.18.2-1.fc38.noarch 31/50 - Cleanup : dnf-4.18.2-1.fc38.noarch 31/50 - Running scriptlet: dnf-4.18.2-1.fc38.noarch 31/50 - Cleanup : python3-dnf-4.18.2-1.fc38.noarch 32/50 - Cleanup : dnf-data-4.18.2-1.fc38.noarch 33/50 - Cleanup : vim-data-2:9.1.113-1.fc38.noarch 34/50 - Cleanup : elfutils-default-yama-scope-0.190-2.fc38.noarch 35/50 - Cleanup : python3-hawkey-0.72.0-1.fc38.x86_64 36/50 - Cleanup : python3-libdnf-0.72.0-1.fc38.x86_64 37/50 - Cleanup : libdnf-0.72.0-1.fc38.x86_64 38/50 - Cleanup : libstdc++-13.2.1-4.fc38.x86_64 39/50 - Cleanup : python3-libs-3.11.7-2.fc38.x86_64 40/50 - Cleanup : python3-3.11.7-2.fc38.x86_64 41/50 - Cleanup : ncurses-libs-6.4-7.20230520.fc38.x86_64 42/50 - Cleanup : ncurses-base-6.4-7.20230520.fc38.noarch 43/50 - Cleanup : expat-2.5.0-2.fc38.x86_64 44/50 - Cleanup : libgcc-13.2.1-4.fc38.x86_64 45/50 - Running scriptlet: libgcc-13.2.1-4.fc38.x86_64 45/50 - Cleanup : libsolv-0.7.27-1.fc38.x86_64 46/50 - Cleanup : libcurl-8.0.1-6.fc38.x86_64 47/50 - Cleanup : elfutils-libelf-0.190-2.fc38.x86_64 48/50 - Cleanup : libgomp-13.2.1-4.fc38.x86_64 49/50 - Cleanup : keyutils-libs-1.6.1-6.fc38.x86_64 50/50 - Running scriptlet: keyutils-libs-1.6.1-6.fc38.x86_64 50/50 - Verifying : curl-8.0.1-7.fc38.x86_64 1/50 - Verifying : curl-8.0.1-6.fc38.x86_64 2/50 - Verifying : dnf-4.19.0-1.fc38.noarch 3/50 - Verifying : dnf-4.18.2-1.fc38.noarch 4/50 - Verifying : dnf-data-4.19.0-1.fc38.noarch 5/50 - Verifying : dnf-data-4.18.2-1.fc38.noarch 6/50 - Verifying : elfutils-default-yama-scope-0.191-1.fc38.noarch 7/50 - Verifying : elfutils-default-yama-scope-0.190-2.fc38.noarch 8/50 - Verifying : elfutils-libelf-0.191-1.fc38.x86_64 9/50 - Verifying : elfutils-libelf-0.190-2.fc38.x86_64 10/50 - Verifying : elfutils-libs-0.191-1.fc38.x86_64 11/50 - Verifying : elfutils-libs-0.190-2.fc38.x86_64 12/50 - Verifying : expat-2.6.0-1.fc38.x86_64 13/50 - Verifying : expat-2.5.0-2.fc38.x86_64 14/50 - Verifying : keyutils-libs-1.6.3-1.fc38.x86_64 15/50 - Verifying : keyutils-libs-1.6.1-6.fc38.x86_64 16/50 - Verifying : libcurl-8.0.1-7.fc38.x86_64 17/50 - Verifying : libcurl-8.0.1-6.fc38.x86_64 18/50 - Verifying : libdnf-0.73.0-1.fc38.x86_64 19/50 - Verifying : libdnf-0.72.0-1.fc38.x86_64 20/50 - Verifying : libgcc-13.2.1-7.fc38.x86_64 21/50 - Verifying : libgcc-13.2.1-4.fc38.x86_64 22/50 - Verifying : libgomp-13.2.1-7.fc38.x86_64 23/50 - Verifying : libgomp-13.2.1-4.fc38.x86_64 24/50 - Verifying : libsolv-0.7.28-1.fc38.x86_64 25/50 - Verifying : libsolv-0.7.27-1.fc38.x86_64 26/50 - Verifying : libstdc++-13.2.1-7.fc38.x86_64 27/50 - Verifying : libstdc++-13.2.1-4.fc38.x86_64 28/50 - Verifying : ncurses-base-6.4-7.20230520.fc38.1.noarch 29/50 - Verifying : ncurses-base-6.4-7.20230520.fc38.noarch 30/50 - Verifying : ncurses-libs-6.4-7.20230520.fc38.1.x86_64 31/50 - Verifying : ncurses-libs-6.4-7.20230520.fc38.x86_64 32/50 - Verifying : python3-3.11.8-2.fc38.x86_64 33/50 - Verifying : python3-3.11.7-2.fc38.x86_64 34/50 - Verifying : python3-dnf-4.19.0-1.fc38.noarch 35/50 - Verifying : python3-dnf-4.18.2-1.fc38.noarch 36/50 - Verifying : python3-hawkey-0.73.0-1.fc38.x86_64 37/50 - Verifying : python3-hawkey-0.72.0-1.fc38.x86_64 38/50 - Verifying : python3-libdnf-0.73.0-1.fc38.x86_64 39/50 - Verifying : python3-libdnf-0.72.0-1.fc38.x86_64 40/50 - Verifying : python3-libs-3.11.8-2.fc38.x86_64 41/50 - Verifying : python3-libs-3.11.7-2.fc38.x86_64 42/50 - Verifying : systemd-libs-253.17-1.fc38.x86_64 43/50 - Verifying : systemd-libs-253.15-2.fc38.x86_64 44/50 - Verifying : vim-data-2:9.1.158-1.fc38.noarch 45/50 - Verifying : vim-data-2:9.1.113-1.fc38.noarch 46/50 - Verifying : vim-minimal-2:9.1.158-1.fc38.x86_64 47/50 - Verifying : vim-minimal-2:9.1.113-1.fc38.x86_64 48/50 - Verifying : yum-4.19.0-1.fc38.noarch 49/50 - Verifying : yum-4.18.2-1.fc38.noarch 50/50 - - Upgraded: - curl-8.0.1-7.fc38.x86_64 - dnf-4.19.0-1.fc38.noarch - dnf-data-4.19.0-1.fc38.noarch - elfutils-default-yama-scope-0.191-1.fc38.noarch - elfutils-libelf-0.191-1.fc38.x86_64 - elfutils-libs-0.191-1.fc38.x86_64 - expat-2.6.0-1.fc38.x86_64 - keyutils-libs-1.6.3-1.fc38.x86_64 - libcurl-8.0.1-7.fc38.x86_64 - libdnf-0.73.0-1.fc38.x86_64 - libgcc-13.2.1-7.fc38.x86_64 - libgomp-13.2.1-7.fc38.x86_64 - libsolv-0.7.28-1.fc38.x86_64 - libstdc++-13.2.1-7.fc38.x86_64 - ncurses-base-6.4-7.20230520.fc38.1.noarch - ncurses-libs-6.4-7.20230520.fc38.1.x86_64 - python3-3.11.8-2.fc38.x86_64 - python3-dnf-4.19.0-1.fc38.noarch - python3-hawkey-0.73.0-1.fc38.x86_64 - python3-libdnf-0.73.0-1.fc38.x86_64 - python3-libs-3.11.8-2.fc38.x86_64 - systemd-libs-253.17-1.fc38.x86_64 - vim-data-2:9.1.158-1.fc38.noarch - vim-minimal-2:9.1.158-1.fc38.x86_64 - yum-4.19.0-1.fc38.noarch - - Complete! - --> 787eba03a2e - STEP 3/3: LABEL velocity.config.system="x86_64" velocity.config.backend="podman" velocity.config.distro="fedora" velocity.image.fedora__38="ff9fa85cf102560cf3fe2014c3c758fbb3809247537abbeab2c4b67c62dda164" - COMMIT localhost/fedora__38__x86_64__fedora:latest - --> 17c3457c281 - Successfully tagged localhost/fedora__38__x86_64__fedora:latest - 17c3457c281309909691b183b77323eb56390792a787e4a319b494d40868c907 - ==> xuoykrdt: IMAGE localhost/fedora__38__x86_64__fedora:latest (fedora@=38) BUILT [0:13:27] + fedora@38-aa51aa7 + + ==> aa51aa7: BUILD fedora@38 ... + ==> aa51aa7: GENERATING SCRIPT ... + SCRIPT: /tmp/velocity/build/fedora-38-aa51aa7/script + Bootstrap: docker + From: docker.io/fedora:38 + + %post + dnf -y upgrade + dnf clean all + ==> aa51aa7: BUILDING ... + #!/usr/bin/env bash + apptainer build --disable-cache /tmp/velocity/build/fedora-38-aa51aa7/aa51aa7.sif /tmp/velocity/build/fedora-38-aa51aa7/script; + Fedora 38 - x86_64 2.3 MB/s | 83 MB 00:35 + Fedora 38 openh264 (From Cisco) - x86_64 2.8 kB/s | 2.6 kB 00:00 + Fedora Modular 38 - x86_64 1.8 MB/s | 2.8 MB 00:01 + Fedora 38 - x86_64 - Updates 2.8 MB/s | 42 MB 00:14 + Fedora Modular 38 - x86_64 - Updates 257 kB/s | 2.2 MB 00:08 + Dependencies resolved. + ================================================================================ + Package Arch Version Repo Size + ================================================================================ + Upgrading: + fedora-release-common noarch 38-37 updates 20 k + fedora-release-container noarch 38-37 updates 10 k + fedora-release-identity-container noarch 38-37 updates 12 k + glibc x86_64 2.37-19.fc38 updates 2.1 M + glibc-common x86_64 2.37-19.fc38 updates 320 k + glibc-minimal-langpack x86_64 2.37-19.fc38 updates 42 k + gnutls x86_64 3.8.5-1.fc38 updates 1.1 M + libnghttp2 x86_64 1.52.0-3.fc38 updates 75 k + python-pip-wheel noarch 22.3.1-4.fc38 updates 1.4 M + python3 x86_64 3.11.9-2.fc38 updates 28 k + python3-libs x86_64 3.11.9-2.fc38 updates 9.6 M + tpm2-tss x86_64 4.0.2-1.fc38 updates 391 k + vim-data noarch 2:9.1.393-1.fc38 updates 23 k + vim-minimal x86_64 2:9.1.393-1.fc38 updates 810 k + Installing weak dependencies: + libxcrypt-compat x86_64 4.4.36-1.fc38 updates 90 k + + Transaction Summary + ================================================================================ + Install 1 Package + Upgrade 14 Packages + + Total download size: 16 M + Downloading Packages: + (1/15): fedora-release-container-38-37.noarch.r 160 kB/s | 10 kB 00:00 + (2/15): fedora-release-common-38-37.noarch.rpm 228 kB/s | 20 kB 00:00 + (3/15): fedora-release-identity-container-38-37 385 kB/s | 12 kB 00:00 + (4/15): libxcrypt-compat-4.4.36-1.fc38.x86_64.r 648 kB/s | 90 kB 00:00 + (5/15): glibc-minimal-langpack-2.37-19.fc38.x86 1.1 MB/s | 42 kB 00:00 + (6/15): glibc-common-2.37-19.fc38.x86_64.rpm 2.4 MB/s | 320 kB 00:00 + (7/15): libnghttp2-1.52.0-3.fc38.x86_64.rpm 1.7 MB/s | 75 kB 00:00 + (8/15): glibc-2.37-19.fc38.x86_64.rpm 8.1 MB/s | 2.1 MB 00:00 + (9/15): python3-3.11.9-2.fc38.x86_64.rpm 686 kB/s | 28 kB 00:00 + (10/15): python-pip-wheel-22.3.1-4.fc38.noarch. 8.8 MB/s | 1.4 MB 00:00 + (11/15): tpm2-tss-4.0.2-1.fc38.x86_64.rpm 6.1 MB/s | 391 kB 00:00 + (12/15): gnutls-3.8.5-1.fc38.x86_64.rpm 3.1 MB/s | 1.1 MB 00:00 + (13/15): vim-data-9.1.393-1.fc38.noarch.rpm 503 kB/s | 23 kB 00:00 + (14/15): vim-minimal-9.1.393-1.fc38.x86_64.rpm 4.3 MB/s | 810 kB 00:00 + (15/15): python3-libs-3.11.9-2.fc38.x86_64.rpm 11 MB/s | 9.6 MB 00:00 + -------------------------------------------------------------------------------- + Total 11 MB/s | 16 MB 00:01 + Running transaction check + Transaction check succeeded. + Running transaction test + Transaction test succeeded. + Running transaction + Preparing : 1/1 + Upgrading : glibc-common-2.37-19.fc38.x86_64 1/29 + Upgrading : glibc-minimal-langpack-2.37-19.fc38.x86_64 2/29 + Running scriptlet: glibc-2.37-19.fc38.x86_64 3/29 + Upgrading : glibc-2.37-19.fc38.x86_64 3/29 + Running scriptlet: glibc-2.37-19.fc38.x86_64 3/29 + Upgrading : fedora-release-identity-container-38-37.noarch 4/29 + Upgrading : fedora-release-container-38-37.noarch 5/29 + Upgrading : fedora-release-common-38-37.noarch 6/29 + Installing : libxcrypt-compat-4.4.36-1.fc38.x86_64 7/29 + Upgrading : python-pip-wheel-22.3.1-4.fc38.noarch 8/29 + Upgrading : python3-3.11.9-2.fc38.x86_64 9/29 + Upgrading : python3-libs-3.11.9-2.fc38.x86_64 10/29 + Upgrading : vim-data-2:9.1.393-1.fc38.noarch 11/29 + Upgrading : vim-minimal-2:9.1.393-1.fc38.x86_64 12/29 + Upgrading : gnutls-3.8.5-1.fc38.x86_64 13/29 + Upgrading : libnghttp2-1.52.0-3.fc38.x86_64 14/29 + Running scriptlet: tpm2-tss-4.0.2-1.fc38.x86_64 15/29 + Upgrading : tpm2-tss-4.0.2-1.fc38.x86_64 15/29 + Cleanup : fedora-release-common-38-36.noarch 16/29 + Cleanup : gnutls-3.8.4-1.fc38.x86_64 17/29 + Cleanup : tpm2-tss-4.0.1-3.fc38.x86_64 18/29 + Cleanup : vim-minimal-2:9.1.309-1.fc38.x86_64 19/29 + Cleanup : libnghttp2-1.52.0-2.fc38.x86_64 20/29 + Cleanup : python3-3.11.8-2.fc38.x86_64 21/29 + Cleanup : fedora-release-container-38-36.noarch 22/29 + Cleanup : fedora-release-identity-container-38-36.noarch 23/29 + Cleanup : vim-data-2:9.1.309-1.fc38.noarch 24/29 + Cleanup : python3-libs-3.11.8-2.fc38.x86_64 25/29 + Cleanup : python-pip-wheel-22.3.1-3.fc38.noarch 26/29 + Cleanup : glibc-2.37-18.fc38.x86_64 27/29 + Cleanup : glibc-minimal-langpack-2.37-18.fc38.x86_64 28/29 + Cleanup : glibc-common-2.37-18.fc38.x86_64 29/29 + Running scriptlet: glibc-common-2.37-18.fc38.x86_64 29/29 + Verifying : libxcrypt-compat-4.4.36-1.fc38.x86_64 1/29 + Verifying : fedora-release-common-38-37.noarch 2/29 + Verifying : fedora-release-common-38-36.noarch 3/29 + Verifying : fedora-release-container-38-37.noarch 4/29 + Verifying : fedora-release-container-38-36.noarch 5/29 + Verifying : fedora-release-identity-container-38-37.noarch 6/29 + Verifying : fedora-release-identity-container-38-36.noarch 7/29 + Verifying : glibc-2.37-19.fc38.x86_64 8/29 + Verifying : glibc-2.37-18.fc38.x86_64 9/29 + Verifying : glibc-common-2.37-19.fc38.x86_64 10/29 + Verifying : glibc-common-2.37-18.fc38.x86_64 11/29 + Verifying : glibc-minimal-langpack-2.37-19.fc38.x86_64 12/29 + Verifying : glibc-minimal-langpack-2.37-18.fc38.x86_64 13/29 + Verifying : gnutls-3.8.5-1.fc38.x86_64 14/29 + Verifying : gnutls-3.8.4-1.fc38.x86_64 15/29 + Verifying : libnghttp2-1.52.0-3.fc38.x86_64 16/29 + Verifying : libnghttp2-1.52.0-2.fc38.x86_64 17/29 + Verifying : python-pip-wheel-22.3.1-4.fc38.noarch 18/29 + Verifying : python-pip-wheel-22.3.1-3.fc38.noarch 19/29 + Verifying : python3-3.11.9-2.fc38.x86_64 20/29 + Verifying : python3-3.11.8-2.fc38.x86_64 21/29 + Verifying : python3-libs-3.11.9-2.fc38.x86_64 22/29 + Verifying : python3-libs-3.11.8-2.fc38.x86_64 23/29 + Verifying : tpm2-tss-4.0.2-1.fc38.x86_64 24/29 + Verifying : tpm2-tss-4.0.1-3.fc38.x86_64 25/29 + Verifying : vim-data-2:9.1.393-1.fc38.noarch 26/29 + Verifying : vim-data-2:9.1.309-1.fc38.noarch 27/29 + Verifying : vim-minimal-2:9.1.393-1.fc38.x86_64 28/29 + Verifying : vim-minimal-2:9.1.309-1.fc38.x86_64 29/29 + + Upgraded: + fedora-release-common-38-37.noarch + fedora-release-container-38-37.noarch + fedora-release-identity-container-38-37.noarch + glibc-2.37-19.fc38.x86_64 + glibc-common-2.37-19.fc38.x86_64 + glibc-minimal-langpack-2.37-19.fc38.x86_64 + gnutls-3.8.5-1.fc38.x86_64 + libnghttp2-1.52.0-3.fc38.x86_64 + python-pip-wheel-22.3.1-4.fc38.noarch + python3-3.11.9-2.fc38.x86_64 + python3-libs-3.11.9-2.fc38.x86_64 + tpm2-tss-4.0.2-1.fc38.x86_64 + vim-data-2:9.1.393-1.fc38.noarch + vim-minimal-2:9.1.393-1.fc38.x86_64 + Installed: + libxcrypt-compat-4.4.36-1.fc38.x86_64 + + Complete! + 42 files removed + ==> aa51aa7: IMAGE /tmp/velocity/build/fedora-38-aa51aa7/aa51aa7.sif (fedora@38) BUILT [0:01:33] + + ==> BUILT: /tmp/velocity/fedora-38__x86_64-fedora.sif Adding Different Versions ######################### So now we have a base Fedora image. That's great but before we move on let's make some different versions of the image -so that we have more options for building later. Go ahead and copy the `fedora/38` directory several times: +so that we have more options for building later. Edit the fedora ``specs.yaml`` and add some versions. -.. code-block:: bash - - cp -rf fedora/38/ fedora/39 - cp -rf fedora/38/ fedora/40 - cp -rf fedora/38/ fedora/41 - -For each of the versions you will need to go in and change the tag on the source. For example `docker.io/fedora:38` -in `fedora/40/template/fedora.vtmp` should be changed to `docker.io/fedora:40`. +.. code-block:: yaml + :caption: spec.yaml -.. code-block:: bash - :caption: VELOCITY_IMAGE_DIR - - . - └── fedora - ├── 38 - │ ├── specifications.yaml - │ └── templates - │ └── fedora.vtmp - ├── 39 - │ ├── specifications.yaml - │ └── templates - │ └── fedora.vtmp - ├── 40 - │ ├── specifications.yaml - │ └── templates - │ └── fedora.vtmp - └── 41 - ├── specifications.yaml - └── templates - └── fedora.vtmp + versions: + - spec: + - 38 + - 39 + - 40 + - 41 + when: distro=fedora .. code-block:: bash user@hostname:~$ velocity avail - ==> System: x86_64 - ==> Backend: podman - ==> Distro: fedora - ==> fedora - 38 - 39 - 40 - 41 + 38 + 39 + 40 + 41 Specifying Version ################## -When building an image Velocity will default to the latest image. To specify a version use `@=` e.g. -fedora\@=40. Versions are compared by splitting the version string up based on periods and then comparing the sections -left to right alphanumerically. +When building an image Velocity will default to the latest image. To specify a version use ``@`` e.g. +``fedora@40``. Versions take the form ``..-``. You can also specify greater than, less +than, and in-between via ``@:``, ``@:`` and ``@:`` respectively. Hello World! ############ @@ -398,525 +264,256 @@ Now let's get a little more complicated. Let's create an image that runs a pytho can give it whatever version you want: .. code-block:: bash - :caption: VELOCITY_IMAGE_DIR - - . - ├── fedora - │ ├── 38 - │ │ ├── specifications.yaml - │ │ └── templates - │ │ └── fedora.vtmp - │ ├── 39 - │ │ ├── specifications.yaml - │ │ └── templates - │ │ └── fedora.vtmp - │ ├── 40 - │ │ ├── specifications.yaml - │ │ └── templates - │ │ └── fedora.vtmp - │ └── 41 - │ ├── specifications.yaml - │ └── templates - │ └── fedora.vtmp - └── hello-world - └── 1.0 - ├── x86_64 - │ └── hello_world.py - ├── specifications.yaml - └── templates - └── fedora.vtmp - -Notice that now there is a new folder called `x86_64` with a python file in it. + :caption: /tmp/velocity/images + + fedora + ├── specs.yaml + └── templates + └── default.vtmp + hello-world + ├── files + │ └── hello_world.py + ├── specs.yaml + └── templates + └── default.vtmp + +Notice that now there is a new folder called ``files`` with a python script in it. .. code-block:: bash - :caption: hello-world/1.0/x86_64/hello_world.py + :caption: hello_world.py #!/usr/bin/env python3 print("Hello, World!") .. code-block:: yaml - :caption: hello-world/1.0/specifications.yaml - - build_specifications: + :caption: specs.yaml - x86_64: - podman: - fedora: - dependencies: - - fedora - variables: - fr: fakeroot + versions: + - spec: 1.0 + dependencies: + - spec: fedora + when: distro=fedora + files: + - name: hello_world.py .. code-block:: text - :caption: hello-world/1.0/templates/fedora.vtmp + :caption: default.vtmp @from - %(__base__) + {{ __base__ }} @copy - hello_world.py /hello_world.py + hello_world.py /hello_world + + @run + dnf -y install python3 + chmod +x /hello_world @entry - /hello_world.py + /hello_world - @label - velocity.image.%(__name__)__%(__tag__) %(__hash__) .. code-block:: bash user@hostname:~$ velocity avail - ==> System: x86_64 - ==> Backend: podman - ==> Distro: fedora - ==> fedora - 38 - 39 - 40 - 41 + 38 + 39 + 40 + 41 ==> hello-world - 1.0 + 1.0 .. code-block:: bash user@hostname:~$ velocity build hello-world -v - ==> System: x86_64 - ==> Backend: podman - ==> Distro: fedora - ==> Build Order: - fedora@=41 - hello-world@=1.0 - - ==> wmrxxohy: BUILD fedora@=41 ... - ==> wmrxxohy: GENERATING SCRIPT ... - SCRIPT: /tmp/velocity/build/wmrxxohy/script - FROM docker.io/fedora:41 - - RUN dnf -y upgrade - - LABEL velocity.config.system="x86_64" \ - velocity.config.backend="podman" \ - velocity.config.distro="fedora" \ - velocity.image.fedora__41="1ad7926d7e542fa521fe4a2eca54aa73caea82958b1f07adf04728b5762063ac" - - ==> wmrxxohy: BUILDING ... - #!/usr/bin/env bash - podman build -f /tmp/velocity/build/wmrxxohy/script -t localhost/wmrxxohy:latest .; - STEP 1/3: FROM docker.io/fedora:41 - STEP 2/3: RUN dnf -y upgrade - Fedora rawhide openh264 (From Cisco) - x86_64 59 B/s | 123 B 00:02 - Fedora - Rawhide - Developmental packages for t 3.1 MB/s | 20 MB 00:06 - Dependencies resolved. - ====================================================================================== - Package Arch Version Repo Size - ====================================================================================== - Upgrading: - audit-libs x86_64 4.0.1-1.fc41 rawhide 126 k - authselect x86_64 1.5.0-5.fc41 rawhide 146 k - authselect-libs x86_64 1.5.0-5.fc41 rawhide 219 k - crypto-policies noarch 20240320-1.git58e3d95.fc41 rawhide 91 k - curl x86_64 8.6.0-7.fc41 rawhide 301 k - dnf noarch 4.19.2-1.fc41 rawhide 503 k - dnf-data noarch 4.19.2-1.fc41 rawhide 40 k - elfutils-default-yama-scope noarch 0.191-5.fc41 rawhide 13 k - elfutils-libelf x86_64 0.191-5.fc41 rawhide 209 k - elfutils-libs x86_64 0.191-5.fc41 rawhide 258 k - expat x86_64 2.6.2-1.fc41 rawhide 113 k - fedora-release-common noarch 41-0.6 rawhide 21 k - fedora-release-container noarch 41-0.6 rawhide 11 k - fedora-release-identity-container noarch 41-0.6 rawhide 12 k - glib2 x86_64 2.80.0-1.fc41 rawhide 3.0 M - glibc x86_64 2.39.9000-10.fc41 rawhide 2.2 M - glibc-common x86_64 2.39.9000-10.fc41 rawhide 393 k - glibc-minimal-langpack x86_64 2.39.9000-10.fc41 rawhide 106 k - gmp x86_64 1:6.3.0-1.fc41 rawhide 317 k - gnupg2 x86_64 2.4.5-1.fc41 rawhide 2.7 M - gnutls x86_64 3.8.4-1.fc41 rawhide 1.1 M - libassuan x86_64 2.5.7-1.fc41 rawhide 67 k - libblkid x86_64 2.40-0.12.fc41 rawhide 125 k - libcurl x86_64 8.6.0-7.fc41 rawhide 345 k - libdnf x86_64 0.73.1-1.fc41 rawhide 697 k - libeconf x86_64 0.6.2-1.fc41 rawhide 32 k - libffi x86_64 3.4.6-1.fc41 rawhide 40 k - libgcc x86_64 14.0.1-0.13.fc41 rawhide 123 k - libgcrypt x86_64 1.10.3-4.fc41 rawhide 504 k - libgomp x86_64 14.0.1-0.13.fc41 rawhide 344 k - libgpg-error x86_64 1.48-1.fc41 rawhide 232 k - libksba x86_64 1.6.6-1.fc41 rawhide 159 k - libmodulemd x86_64 2.15.0-9.fc41 rawhide 233 k - libmount x86_64 2.40-0.12.fc41 rawhide 155 k - libnghttp2 x86_64 1.60.0-2.fc41 rawhide 76 k - librepo x86_64 1.17.1-1.fc41 rawhide 99 k - libreport-filesystem noarch 2.17.15-1.fc41 rawhide 14 k - libsmartcols x86_64 2.40-0.12.fc41 rawhide 84 k - libssh x86_64 0.10.6-6.fc41 rawhide 212 k - libssh-config noarch 0.10.6-6.fc41 rawhide 9.1 k - libstdc++ x86_64 14.0.1-0.13.fc41 rawhide 881 k - libtirpc x86_64 1.3.4-1.rc3.fc41 rawhide 92 k - libunistring x86_64 1.1-7.fc41 rawhide 545 k - libuuid x86_64 2.40-0.12.fc41 rawhide 29 k - libxml2 x86_64 2.12.6-1.fc41 rawhide 686 k - libzstd x86_64 1.5.6-1.fc41 rawhide 309 k - npth x86_64 1.7-1.fc41 rawhide 25 k - openssl-libs x86_64 1:3.2.1-3.fc41 rawhide 2.3 M - pcre2 x86_64 10.43-1.fc41 rawhide 242 k - pcre2-syntax noarch 10.43-1.fc41 rawhide 149 k - python-pip-wheel noarch 24.0-2.fc41 rawhide 1.5 M - python3 x86_64 3.12.2-3.fc41 rawhide 27 k - python3-dnf noarch 4.19.2-1.fc41 rawhide 594 k - python3-hawkey x86_64 0.73.1-1.fc41 rawhide 105 k - python3-libdnf x86_64 0.73.1-1.fc41 rawhide 847 k - python3-libs x86_64 3.12.2-3.fc41 rawhide 9.1 M - shadow-utils x86_64 2:4.15.1-2.fc41 rawhide 1.3 M - sqlite-libs x86_64 3.45.2-1.fc41 rawhide 706 k - systemd-libs x86_64 255.4-1.fc41 rawhide 708 k - tzdata noarch 2024a-4.fc41 rawhide 716 k - util-linux-core x86_64 2.40-0.12.fc41 rawhide 537 k - vim-data noarch 2:9.1.181-1.fc41 rawhide 23 k - vim-minimal x86_64 2:9.1.181-1.fc41 rawhide 807 k - xz-libs x86_64 1:5.4.6-3.fc41 rawhide 110 k - yum noarch 4.19.2-1.fc41 rawhide 37 k - - Transaction Summary - ====================================================================================== - Upgrade 65 Packages - - Total download size: 38 M - Downloading Packages: - (1/65): audit-libs-4.0.1-1.fc41.x86_64.rpm 136 kB/s | 126 kB 00:00 - (2/65): authselect-1.5.0-5.fc41.x86_64.rpm 153 kB/s | 146 kB 00:00 - (3/65): crypto-policies-20240320-1.git58e3d95.f 623 kB/s | 91 kB 00:00 - (4/65): authselect-libs-1.5.0-5.fc41.x86_64.rpm 199 kB/s | 219 kB 00:01 - [MIRROR] dnf-4.19.2-1.fc41.noarch.rpm: Status code: 404 for http://mirror.math.princeton.edu/pub/fedora/linux/development/rawhide/Everything/x86_64/os/Packages/d/dnf-4.19.2-1.fc41.noarch.rpm (IP: 128.112.18.21) - [MIRROR] dnf-data-4.19.2-1.fc41.noarch.rpm: Status code: 404 for http://mirror.math.princeton.edu/pub/fedora/linux/development/rawhide/Everything/x86_64/os/Packages/d/dnf-data-4.19.2-1.fc41.noarch.rpm (IP: 128.112.18.21) - (5/65): curl-8.6.0-7.fc41.x86_64.rpm 919 kB/s | 301 kB 00:00 - (6/65): elfutils-default-yama-scope-0.191-5.fc4 105 kB/s | 13 kB 00:00 - (7/65): elfutils-libelf-0.191-5.fc41.x86_64.rpm 739 kB/s | 209 kB 00:00 - (8/65): dnf-data-4.19.2-1.fc41.noarch.rpm 51 kB/s | 40 kB 00:00 - (9/65): elfutils-libs-0.191-5.fc41.x86_64.rpm 980 kB/s | 258 kB 00:00 - (10/65): expat-2.6.2-1.fc41.x86_64.rpm 787 kB/s | 113 kB 00:00 - (11/65): fedora-release-common-41-0.6.noarch.rp 165 kB/s | 21 kB 00:00 - (12/65): fedora-release-container-41-0.6.noarch 87 kB/s | 11 kB 00:00 - (13/65): fedora-release-identity-container-41-0 77 kB/s | 12 kB 00:00 - (14/65): dnf-4.19.2-1.fc41.noarch.rpm 402 kB/s | 503 kB 00:01 - (15/65): glib2-2.80.0-1.fc41.x86_64.rpm 4.8 MB/s | 3.0 MB 00:00 - (16/65): glibc-minimal-langpack-2.39.9000-10.fc 783 kB/s | 106 kB 00:00 - (17/65): gmp-6.3.0-1.fc41.x86_64.rpm 1.7 MB/s | 317 kB 00:00 - (18/65): glibc-common-2.39.9000-10.fc41.x86_64. 507 kB/s | 393 kB 00:00 - (19/65): gnupg2-2.4.5-1.fc41.x86_64.rpm 8.2 MB/s | 2.7 MB 00:00 - (20/65): libassuan-2.5.7-1.fc41.x86_64.rpm 458 kB/s | 67 kB 00:00 - (21/65): libblkid-2.40-0.12.fc41.x86_64.rpm 687 kB/s | 125 kB 00:00 - (22/65): glibc-2.39.9000-10.fc41.x86_64.rpm 1.0 MB/s | 2.2 MB 00:02 - [MIRROR] libdnf-0.73.1-1.fc41.x86_64.rpm: Status code: 404 for http://mirror.math.princeton.edu/pub/fedora/linux/development/rawhide/Everything/x86_64/os/Packages/l/libdnf-0.73.1-1.fc41.x86_64.rpm (IP: 128.112.18.21) - (23/65): gnutls-3.8.4-1.fc41.x86_64.rpm 668 kB/s | 1.1 MB 00:01 - (24/65): libeconf-0.6.2-1.fc41.x86_64.rpm 249 kB/s | 32 kB 00:00 - (25/65): libdnf-0.73.1-1.fc41.x86_64.rpm 1.6 MB/s | 697 kB 00:00 - (26/65): libffi-3.4.6-1.fc41.x86_64.rpm 205 kB/s | 40 kB 00:00 - (27/65): libgcc-14.0.1-0.13.fc41.x86_64.rpm 391 kB/s | 123 kB 00:00 - (28/65): libgomp-14.0.1-0.13.fc41.x86_64.rpm 642 kB/s | 344 kB 00:00 - (29/65): libgcrypt-1.10.3-4.fc41.x86_64.rpm 613 kB/s | 504 kB 00:00 - (30/65): libgpg-error-1.48-1.fc41.x86_64.rpm 579 kB/s | 232 kB 00:00 - (31/65): libksba-1.6.6-1.fc41.x86_64.rpm 580 kB/s | 159 kB 00:00 - (32/65): libcurl-8.6.0-7.fc41.x86_64.rpm 140 kB/s | 345 kB 00:02 - (33/65): libmodulemd-2.15.0-9.fc41.x86_64.rpm 406 kB/s | 233 kB 00:00 - (34/65): libmount-2.40-0.12.fc41.x86_64.rpm 271 kB/s | 155 kB 00:00 - (35/65): libreport-filesystem-2.17.15-1.fc41.no 82 kB/s | 14 kB 00:00 - (36/65): librepo-1.17.1-1.fc41.x86_64.rpm 501 kB/s | 99 kB 00:00 - (37/65): libsmartcols-2.40-0.12.fc41.x86_64.rpm 323 kB/s | 84 kB 00:00 - (38/65): libssh-0.10.6-6.fc41.x86_64.rpm 744 kB/s | 212 kB 00:00 - (39/65): libssh-config-0.10.6-6.fc41.noarch.rpm 74 kB/s | 9.1 kB 00:00 - (40/65): libtirpc-1.3.4-1.rc3.fc41.x86_64.rpm 357 kB/s | 92 kB 00:00 - (41/65): libstdc++-14.0.1-0.13.fc41.x86_64.rpm 837 kB/s | 881 kB 00:01 - (42/65): libnghttp2-1.60.0-2.fc41.x86_64.rpm 35 kB/s | 76 kB 00:02 - (43/65): libuuid-2.40-0.12.fc41.x86_64.rpm 208 kB/s | 29 kB 00:00 - (44/65): libunistring-1.1-7.fc41.x86_64.rpm 534 kB/s | 545 kB 00:01 - (45/65): npth-1.7-1.fc41.x86_64.rpm 187 kB/s | 25 kB 00:00 - (46/65): libzstd-1.5.6-1.fc41.x86_64.rpm 767 kB/s | 309 kB 00:00 - (47/65): pcre2-10.43-1.fc41.x86_64.rpm 611 kB/s | 242 kB 00:00 - (48/65): pcre2-syntax-10.43-1.fc41.noarch.rpm 386 kB/s | 149 kB 00:00 - (49/65): python-pip-wheel-24.0-2.fc41.noarch.rp 640 kB/s | 1.5 MB 00:02 - (50/65): python3-3.12.2-3.fc41.x86_64.rpm 203 kB/s | 27 kB 00:00 - [MIRROR] python3-dnf-4.19.2-1.fc41.noarch.rpm: Status code: 404 for http://mirror.math.princeton.edu/pub/fedora/linux/development/rawhide/Everything/x86_64/os/Packages/p/python3-dnf-4.19.2-1.fc41.noarch.rpm (IP: 128.112.18.21) - (51/65): libxml2-2.12.6-1.fc41.x86_64.rpm 168 kB/s | 686 kB 00:04 - (52/65): openssl-libs-3.2.1-3.fc41.x86_64.rpm 554 kB/s | 2.3 MB 00:04 - (53/65): python3-hawkey-0.73.1-1.fc41.x86_64.rp 130 kB/s | 105 kB 00:00 - (54/65): python3-dnf-4.19.2-1.fc41.noarch.rpm 425 kB/s | 594 kB 00:01 - (55/65): shadow-utils-4.15.1-2.fc41.x86_64.rpm 3.5 MB/s | 1.3 MB 00:00 - (56/65): sqlite-libs-3.45.2-1.fc41.x86_64.rpm 2.8 MB/s | 706 kB 00:00 - (57/65): python3-libdnf-0.73.1-1.fc41.x86_64.rp 678 kB/s | 847 kB 00:01 - (58/65): systemd-libs-255.4-1.fc41.x86_64.rpm 2.9 MB/s | 708 kB 00:00 - (59/65): util-linux-core-2.40-0.12.fc41.x86_64. 3.0 MB/s | 537 kB 00:00 - (60/65): tzdata-2024a-4.fc41.noarch.rpm 1.5 MB/s | 716 kB 00:00 - (61/65): vim-data-9.1.181-1.fc41.noarch.rpm 125 kB/s | 23 kB 00:00 - (62/65): xz-libs-5.4.6-3.fc41.x86_64.rpm 530 kB/s | 110 kB 00:00 - (63/65): yum-4.19.2-1.fc41.noarch.rpm 215 kB/s | 37 kB 00:00 - (64/65): python3-libs-3.12.2-3.fc41.x86_64.rpm 3.5 MB/s | 9.1 MB 00:02 - (65/65): vim-minimal-9.1.181-1.fc41.x86_64.rpm 218 kB/s | 807 kB 00:03 - -------------------------------------------------------------------------------- - Total 2.0 MB/s | 38 MB 00:19 - Running transaction check - Transaction check succeeded. - Running transaction test - Transaction test succeeded. - Running transaction - Preparing : 1/1 - Upgrading : libgcc-14.0.1-0.13.fc41.x86_64 1/130 - Running scriptlet: libgcc-14.0.1-0.13.fc41.x86_64 1/130 - Upgrading : tzdata-2024a-4.fc41.noarch 2/130 - Upgrading : crypto-policies-20240320-1.git58e3d95.fc41.noarc 3/130 - Running scriptlet: crypto-policies-20240320-1.git58e3d95.fc41.noarc 3/130 - Upgrading : glibc-common-2.39.9000-10.fc41.x86_64 4/130 - Upgrading : glibc-minimal-langpack-2.39.9000-10.fc41.x86_64 5/130 - Running scriptlet: glibc-2.39.9000-10.fc41.x86_64 6/130 - Upgrading : glibc-2.39.9000-10.fc41.x86_64 6/130 - Running scriptlet: glibc-2.39.9000-10.fc41.x86_64 6/130 - Upgrading : libgpg-error-1.48-1.fc41.x86_64 7/130 - Upgrading : libuuid-2.40-0.12.fc41.x86_64 8/130 - Upgrading : openssl-libs-1:3.2.1-3.fc41.x86_64 9/130 - Upgrading : xz-libs-1:5.4.6-3.fc41.x86_64 10/130 - Upgrading : libsmartcols-2.40-0.12.fc41.x86_64 11/130 - Upgrading : libstdc++-14.0.1-0.13.fc41.x86_64 12/130 - Upgrading : libzstd-1.5.6-1.fc41.x86_64 13/130 - Upgrading : sqlite-libs-3.45.2-1.fc41.x86_64 14/130 - Upgrading : libblkid-2.40-0.12.fc41.x86_64 15/130 - Upgrading : libmount-2.40-0.12.fc41.x86_64 16/130 - Upgrading : libffi-3.4.6-1.fc41.x86_64 17/130 - Upgrading : fedora-release-identity-container-41-0.6.noarch 18/130 - Upgrading : fedora-release-container-41-0.6.noarch 19/130 - Upgrading : fedora-release-common-41-0.6.noarch 20/130 - Upgrading : elfutils-libelf-0.191-5.fc41.x86_64 21/130 - Upgrading : systemd-libs-255.4-1.fc41.x86_64 22/130 - Upgrading : libxml2-2.12.6-1.fc41.x86_64 23/130 - Upgrading : libassuan-2.5.7-1.fc41.x86_64 24/130 - Upgrading : libgcrypt-1.10.3-4.fc41.x86_64 25/130 - Upgrading : libksba-1.6.6-1.fc41.x86_64 26/130 - Upgrading : audit-libs-4.0.1-1.fc41.x86_64 27/130 - Upgrading : authselect-libs-1.5.0-5.fc41.x86_64 28/130 - Upgrading : expat-2.6.2-1.fc41.x86_64 29/130 - Upgrading : gmp-1:6.3.0-1.fc41.x86_64 30/130 - Upgrading : libeconf-0.6.2-1.fc41.x86_64 31/130 - Upgrading : libnghttp2-1.60.0-2.fc41.x86_64 32/130 - Upgrading : libtirpc-1.3.4-1.rc3.fc41.x86_64 33/130 - Upgrading : libunistring-1.1-7.fc41.x86_64 34/130 - Upgrading : gnutls-3.8.4-1.fc41.x86_64 35/130 - Upgrading : npth-1.7-1.fc41.x86_64 36/130 - Upgrading : vim-data-2:9.1.181-1.fc41.noarch 37/130 - Upgrading : python-pip-wheel-24.0-2.fc41.noarch 38/130 - Upgrading : python3-3.12.2-3.fc41.x86_64 39/130 - Upgrading : python3-libs-3.12.2-3.fc41.x86_64 40/130 - Upgrading : pcre2-syntax-10.43-1.fc41.noarch 41/130 - Upgrading : pcre2-10.43-1.fc41.x86_64 42/130 - Upgrading : glib2-2.80.0-1.fc41.x86_64 43/130 - Upgrading : libmodulemd-2.15.0-9.fc41.x86_64 44/130 - Upgrading : libssh-config-0.10.6-6.fc41.noarch 45/130 - Upgrading : libssh-0.10.6-6.fc41.x86_64 46/130 - Upgrading : libcurl-8.6.0-7.fc41.x86_64 47/130 - Upgrading : librepo-1.17.1-1.fc41.x86_64 48/130 - Upgrading : libdnf-0.73.1-1.fc41.x86_64 49/130 - Upgrading : python3-libdnf-0.73.1-1.fc41.x86_64 50/130 - Upgrading : python3-hawkey-0.73.1-1.fc41.x86_64 51/130 - Upgrading : libreport-filesystem-2.17.15-1.fc41.noarch 52/130 - Upgrading : dnf-data-4.19.2-1.fc41.noarch 53/130 - Upgrading : python3-dnf-4.19.2-1.fc41.noarch 54/130 - Upgrading : dnf-4.19.2-1.fc41.noarch 55/130 - Running scriptlet: dnf-4.19.2-1.fc41.noarch 55/130 - Upgrading : elfutils-default-yama-scope-0.191-5.fc41.noarch 56/130 - Running scriptlet: elfutils-default-yama-scope-0.191-5.fc41.noarch 56/130 - Upgrading : elfutils-libs-0.191-5.fc41.x86_64 57/130 - Upgrading : yum-4.19.2-1.fc41.noarch 58/130 - Upgrading : curl-8.6.0-7.fc41.x86_64 59/130 - Upgrading : vim-minimal-2:9.1.181-1.fc41.x86_64 60/130 - Upgrading : gnupg2-2.4.5-1.fc41.x86_64 61/130 - Upgrading : shadow-utils-2:4.15.1-2.fc41.x86_64 62/130 - Upgrading : authselect-1.5.0-5.fc41.x86_64 63/130 - Upgrading : util-linux-core-2.40-0.12.fc41.x86_64 64/130 - Upgrading : libgomp-14.0.1-0.13.fc41.x86_64 65/130 - Cleanup : util-linux-core-2.40-0.9.rc1.fc41.x86_64 66/130 - Cleanup : systemd-libs-255.3-1.fc40.x86_64 67/130 - Cleanup : gnupg2-2.4.4-1.fc40.x86_64 68/130 - Cleanup : elfutils-libs-0.190-6.fc40.x86_64 69/130 - Cleanup : shadow-utils-2:4.14.0-6.fc40.x86_64 70/130 - Cleanup : vim-minimal-2:9.1.113-1.fc41.x86_64 71/130 - Running scriptlet: authselect-1.5.0-3.fc40.x86_64 72/130 - Cleanup : authselect-1.5.0-3.fc40.x86_64 72/130 - Cleanup : curl-8.6.0-6.fc40.x86_64 73/130 - Cleanup : fedora-release-common-41-0.1.noarch 74/130 - Cleanup : libgcrypt-1.10.3-3.fc40.x86_64 75/130 - Cleanup : elfutils-libelf-0.190-6.fc40.x86_64 76/130 - Cleanup : libassuan-2.5.6-4.fc40.x86_64 77/130 - Cleanup : authselect-libs-1.5.0-3.fc40.x86_64 78/130 - Cleanup : audit-libs-4.0-8.fc40.x86_64 79/130 - Cleanup : libksba-1.6.5-3.fc40.x86_64 80/130 - Cleanup : libgpg-error-1.47-4.fc40.x86_64 81/130 - Cleanup : libeconf-0.5.2-3.fc40.x86_64 82/130 - Cleanup : libgomp-14.0.1-0.6.fc40.x86_64 83/130 - Cleanup : fedora-release-container-41-0.1.noarch 84/130 - Cleanup : yum-4.19.0-1.fc40.noarch 85/130 - Cleanup : libzstd-1.5.5-5.fc40.x86_64 86/130 - Cleanup : npth-1.6-18.fc40.x86_64 87/130 - Running scriptlet: dnf-4.19.0-1.fc40.noarch 88/130 - Cleanup : dnf-4.19.0-1.fc40.noarch 88/130 - Running scriptlet: dnf-4.19.0-1.fc40.noarch 88/130 - Cleanup : python3-dnf-4.19.0-1.fc40.noarch 89/130 - Cleanup : dnf-data-4.19.0-1.fc40.noarch 90/130 - Cleanup : libreport-filesystem-2.17.14-1.fc40.noarch 91/130 - Cleanup : fedora-release-identity-container-41-0.1.noarch 92/130 - Cleanup : vim-data-2:9.1.113-1.fc41.noarch 93/130 - Cleanup : elfutils-default-yama-scope-0.190-6.fc40.noarch 94/130 - Cleanup : python3-hawkey-0.73.0-1.fc40.x86_64 95/130 - Cleanup : python3-libdnf-0.73.0-1.fc40.x86_64 96/130 - Cleanup : python3-libs-3.12.2-1.fc40.x86_64 97/130 - Cleanup : python3-3.12.2-1.fc40.x86_64 98/130 - Cleanup : libdnf-0.73.0-1.fc40.x86_64 99/130 - Cleanup : python-pip-wheel-23.3.2-1.fc40.noarch 100/130 - Cleanup : libstdc++-14.0.1-0.6.fc40.x86_64 101/130 - Cleanup : librepo-1.17.0-3.fc40.x86_64 102/130 - Cleanup : libcurl-8.6.0-6.fc40.x86_64 103/130 - Cleanup : libxml2-2.12.5-1.fc40.x86_64 104/130 - Cleanup : libssh-0.10.6-4.fc40.x86_64 105/130 - Cleanup : openssl-libs-1:3.2.1-2.fc40.x86_64 106/130 - Cleanup : sqlite-libs-3.45.1-2.fc40.x86_64 107/130 - Cleanup : libtirpc-1.3.4-1.rc2.fc40.2.x86_64 108/130 - Cleanup : libmodulemd-2.15.0-8.fc40.x86_64 109/130 - Cleanup : glib2-2.79.1-1.fc40.x86_64 110/130 - Cleanup : libmount-2.40-0.9.rc1.fc41.x86_64 111/130 - Cleanup : gnutls-3.8.3-2.fc40.x86_64 112/130 - Cleanup : libblkid-2.40-0.9.rc1.fc41.x86_64 113/130 - Cleanup : libuuid-2.40-0.9.rc1.fc41.x86_64 114/130 - Cleanup : xz-libs-5.4.6-1.fc40.x86_64 115/130 - Cleanup : libsmartcols-2.40-0.9.rc1.fc41.x86_64 116/130 - Cleanup : expat-2.6.0-1.fc41.x86_64 117/130 - Cleanup : gmp-1:6.2.1-8.fc40.x86_64 118/130 - Cleanup : libunistring-1.1-7.fc40.x86_64 119/130 - Cleanup : libffi-3.4.4-7.fc40.x86_64 120/130 - Cleanup : pcre2-10.42-2.fc40.2.x86_64 121/130 - Cleanup : libnghttp2-1.59.0-2.fc40.x86_64 122/130 - Cleanup : pcre2-syntax-10.42-2.fc40.2.noarch 123/130 - Cleanup : crypto-policies-20240201-1.git9f501f3.fc40.noarc 124/130 - Cleanup : libssh-config-0.10.6-4.fc40.noarch 125/130 - Cleanup : glibc-2.39.9000-1.fc41.x86_64 126/130 - Cleanup : glibc-minimal-langpack-2.39.9000-1.fc41.x86_64 127/130 - Cleanup : glibc-common-2.39.9000-1.fc41.x86_64 128/130 - Cleanup : tzdata-2024a-2.fc40.noarch 129/130 - Cleanup : libgcc-14.0.1-0.6.fc40.x86_64 130/130 - Running scriptlet: libgcc-14.0.1-0.6.fc40.x86_64 130/130 - Running scriptlet: authselect-libs-1.5.0-5.fc41.x86_64 130/130 - Running scriptlet: libgcc-14.0.1-0.6.fc40.x86_64 130/130 - - Upgraded: - audit-libs-4.0.1-1.fc41.x86_64 - authselect-1.5.0-5.fc41.x86_64 - authselect-libs-1.5.0-5.fc41.x86_64 - crypto-policies-20240320-1.git58e3d95.fc41.noarch - curl-8.6.0-7.fc41.x86_64 - dnf-4.19.2-1.fc41.noarch - dnf-data-4.19.2-1.fc41.noarch - elfutils-default-yama-scope-0.191-5.fc41.noarch - elfutils-libelf-0.191-5.fc41.x86_64 - elfutils-libs-0.191-5.fc41.x86_64 - expat-2.6.2-1.fc41.x86_64 - fedora-release-common-41-0.6.noarch - fedora-release-container-41-0.6.noarch - fedora-release-identity-container-41-0.6.noarch - glib2-2.80.0-1.fc41.x86_64 - glibc-2.39.9000-10.fc41.x86_64 - glibc-common-2.39.9000-10.fc41.x86_64 - glibc-minimal-langpack-2.39.9000-10.fc41.x86_64 - gmp-1:6.3.0-1.fc41.x86_64 - gnupg2-2.4.5-1.fc41.x86_64 - gnutls-3.8.4-1.fc41.x86_64 - libassuan-2.5.7-1.fc41.x86_64 - libblkid-2.40-0.12.fc41.x86_64 - libcurl-8.6.0-7.fc41.x86_64 - libdnf-0.73.1-1.fc41.x86_64 - libeconf-0.6.2-1.fc41.x86_64 - libffi-3.4.6-1.fc41.x86_64 - libgcc-14.0.1-0.13.fc41.x86_64 - libgcrypt-1.10.3-4.fc41.x86_64 - libgomp-14.0.1-0.13.fc41.x86_64 - libgpg-error-1.48-1.fc41.x86_64 - libksba-1.6.6-1.fc41.x86_64 - libmodulemd-2.15.0-9.fc41.x86_64 - libmount-2.40-0.12.fc41.x86_64 - libnghttp2-1.60.0-2.fc41.x86_64 - librepo-1.17.1-1.fc41.x86_64 - libreport-filesystem-2.17.15-1.fc41.noarch - libsmartcols-2.40-0.12.fc41.x86_64 - libssh-0.10.6-6.fc41.x86_64 - libssh-config-0.10.6-6.fc41.noarch - libstdc++-14.0.1-0.13.fc41.x86_64 - libtirpc-1.3.4-1.rc3.fc41.x86_64 - libunistring-1.1-7.fc41.x86_64 - libuuid-2.40-0.12.fc41.x86_64 - libxml2-2.12.6-1.fc41.x86_64 - libzstd-1.5.6-1.fc41.x86_64 - npth-1.7-1.fc41.x86_64 - openssl-libs-1:3.2.1-3.fc41.x86_64 - pcre2-10.43-1.fc41.x86_64 - pcre2-syntax-10.43-1.fc41.noarch - python-pip-wheel-24.0-2.fc41.noarch - python3-3.12.2-3.fc41.x86_64 - python3-dnf-4.19.2-1.fc41.noarch - python3-hawkey-0.73.1-1.fc41.x86_64 - python3-libdnf-0.73.1-1.fc41.x86_64 - python3-libs-3.12.2-3.fc41.x86_64 - shadow-utils-2:4.15.1-2.fc41.x86_64 - sqlite-libs-3.45.2-1.fc41.x86_64 - systemd-libs-255.4-1.fc41.x86_64 - tzdata-2024a-4.fc41.noarch - util-linux-core-2.40-0.12.fc41.x86_64 - vim-data-2:9.1.181-1.fc41.noarch - vim-minimal-2:9.1.181-1.fc41.x86_64 - xz-libs-1:5.4.6-3.fc41.x86_64 - yum-4.19.2-1.fc41.noarch - - Complete! - --> 43089713ea9 - STEP 3/3: LABEL velocity.config.system="x86_64" velocity.config.backend="podman" velocity.config.distro="fedora" velocity.image.fedora__41="1ad7926d7e542fa521fe4a2eca54aa73caea82958b1f07adf04728b5762063ac" - COMMIT localhost/wmrxxohy:latest - --> 49321c240b5 - Successfully tagged localhost/wmrxxohy:latest - 49321c240b522a0d66cd30b62addc99e87b5861e2e6c27f6d0136968c73be5aa - ==> wmrxxohy: IMAGE localhost/wmrxxohy:latest (fedora@=41) BUILT [0:01:02] - - ==> bdvdbcor: BUILD hello-world@=1.0 ... - ==> bdvdbcor: COPYING FILES ... - FILE: /home/xjv/tmp/hello-world/1.0/x86_64/hello_world.py -> /tmp/velocity/build/bdvdbcor/hello_world.py - ==> bdvdbcor: GENERATING SCRIPT ... - SCRIPT: /tmp/velocity/build/bdvdbcor/script - FROM localhost/wmrxxohy:latest - - COPY hello_world.py /hello_world.py - - LABEL velocity.image.hello-world__1.0="1b080f458c34ae2759d1eb3e8464d3d508e3bcb981a476f70709b0f20b6218bb" - - ENTRYPOINT ['/hello_world.py'] - - ==> bdvdbcor: BUILDING ... - #!/usr/bin/env bash - podman build -f /tmp/velocity/build/bdvdbcor/script -t localhost/hello-world__1.0__x86_64__fedora:latest .; - STEP 1/4: FROM localhost/wmrxxohy:latest - STEP 2/4: COPY hello_world.py /hello_world.py - --> fa1896e8689 - STEP 3/4: LABEL velocity.image.hello-world__1.0="1b080f458c34ae2759d1eb3e8464d3d508e3bcb981a476f70709b0f20b6218bb" - --> d58637575df - STEP 4/4: ENTRYPOINT ['/hello_world.py'] - COMMIT localhost/hello-world__1.0__x86_64__fedora:latest - --> 21211d9de40 - Successfully tagged localhost/hello-world__1.0__x86_64__fedora:latest - 21211d9de40aa6c0cb6b625e6f4fed265c88f9a4be09c4edcd93a78360580ecf - ==> bdvdbcor: IMAGE localhost/hello-world__1.0__x86_64__fedora:latest (hello-world@=1.0) BUILT [0:00:03] + fedora@41-8a9a360 + hello-world@1.0-de9c02b + + ==> 8a9a360: BUILD fedora@41 ... + ==> 8a9a360: GENERATING SCRIPT ... + SCRIPT: /tmp/velocity/build/fedora-41-8a9a360/script + Bootstrap: docker + From: docker.io/fedora:41 + + %post + dnf -y upgrade + dnf clean all + ==> 8a9a360: BUILDING ... + #!/usr/bin/env bash + apptainer build --disable-cache /tmp/velocity/build/fedora-41-8a9a360/8a9a360.sif /tmp/velocity/build/fedora-41-8a9a360/script; + Updating and loading repositories: + Fedora 41 openh264 (From Cisco) - x86_ 100% | 6.5 KiB/s | 4.8 KiB | 00m01s + Fedora 41 - x86_64 - Test Updates 100% | 1.8 MiB/s | 3.4 MiB | 00m02s + Fedora 41 - x86_64 100% | 13.9 MiB/s | 35.9 MiB | 00m03s + Fedora 41 - x86_64 - Updates 100% | 50.3 KiB/s | 31.9 KiB | 00m01s + Repositories loaded. + Package Arch Version Repository Size + Upgrading: + libgcc x86_64 14.2.1-3.fc41 updates-testing 274.6 KiB + replacing libgcc x86_64 14.2.1-1.fc41 34086d2996104518800c8d7dcc6139a1 274.6 KiB + libgomp x86_64 14.2.1-3.fc41 updates-testing 523.5 KiB + replacing libgomp x86_64 14.2.1-1.fc41 34086d2996104518800c8d7dcc6139a1 523.4 KiB + libstdc++ x86_64 14.2.1-3.fc41 updates-testing 2.8 MiB + replacing libstdc++ x86_64 14.2.1-1.fc41 34086d2996104518800c8d7dcc6139a1 2.8 MiB + openssl-libs x86_64 1:3.2.2-7.fc41 updates-testing 7.8 MiB + replacing openssl-libs x86_64 1:3.2.2-5.fc41 34086d2996104518800c8d7dcc6139a1 7.8 MiB + rpm x86_64 4.19.94-1.fc41 updates-testing 3.1 MiB + replacing rpm x86_64 4.19.92-6.fc41 34086d2996104518800c8d7dcc6139a1 3.1 MiB + rpm-build-libs x86_64 4.19.94-1.fc41 updates-testing 206.7 KiB + replacing rpm-build-libs x86_64 4.19.92-6.fc41 34086d2996104518800c8d7dcc6139a1 206.7 KiB + rpm-libs x86_64 4.19.94-1.fc41 updates-testing 721.9 KiB + replacing rpm-libs x86_64 4.19.92-6.fc41 34086d2996104518800c8d7dcc6139a1 721.9 KiB + systemd-libs x86_64 256.6-1.fc41 updates-testing 2.0 MiB + replacing systemd-libs x86_64 256.5-1.fc41 34086d2996104518800c8d7dcc6139a1 2.0 MiB + zlib-ng-compat x86_64 2.1.7-3.fc41 updates-testing 134.0 KiB + replacing zlib-ng-compat x86_64 2.1.7-2.fc41 34086d2996104518800c8d7dcc6139a1 134.0 KiB + + Transaction Summary: + Upgrading: 9 packages + Replacing: 9 packages + + Total size of inbound packages is 5 MiB. Need to download 5 MiB. + After this operation 2 KiB will be used (install 17 MiB, remove 17 MiB). + [1/9] libgcc-0:14.2.1-3.fc41.x86_64 100% | 132.2 KiB/s | 133.3 KiB | 00m01s + [2/9] libstdc++-0:14.2.1-3.fc41.x86_64 100% | 583.3 KiB/s | 887.8 KiB | 00m02s + [3/9] libgomp-0:14.2.1-3.fc41.x86_64 100% | 208.5 KiB/s | 354.1 KiB | 00m02s + [4/9] rpm-0:4.19.94-1.fc41.x86_64 100% | 1.6 MiB/s | 547.6 KiB | 00m00s + [5/9] rpm-build-libs-0:4.19.94-1.fc41.x 100% | 2.1 MiB/s | 99.1 KiB | 00m00s + [6/9] openssl-libs-1:3.2.2-7.fc41.x86_6 100% | 2.6 MiB/s | 2.3 MiB | 00m01s + [7/9] rpm-libs-0:4.19.94-1.fc41.x86_64 100% | 1.1 MiB/s | 309.5 KiB | 00m00s + [8/9] zlib-ng-compat-0:2.1.7-3.fc41.x86 100% | 1.0 MiB/s | 77.7 KiB | 00m00s + [9/9] systemd-libs-0:256.6-1.fc41.x86_6 100% | 2.6 MiB/s | 730.9 KiB | 00m00s + -------------------------------------------------------------------------------- + [9/9] Total 100% | 2.2 MiB/s | 5.4 MiB | 00m02s + Running transaction + [ 1/20] Verify package files 100% | 750.0 B/s | 9.0 B | 00m00s + [ 2/20] Prepare transaction 100% | 1.6 KiB/s | 18.0 B | 00m00s + [ 3/20] Upgrading libgcc-0:14.2.1-3.fc4 100% | 14.2 MiB/s | 276.3 KiB | 00m00s + >>> Running post-install scriptlet: libgcc-0:14.2.1-3.fc41.x86_64 + >>> Stop post-install scriptlet: libgcc-0:14.2.1-3.fc41.x86_64 + [ 4/20] Upgrading zlib-ng-compat-0:2.1. 100% | 32.9 MiB/s | 134.8 KiB | 00m00s + [ 5/20] Upgrading rpm-libs-0:4.19.94-1. 100% | 117.7 MiB/s | 723.4 KiB | 00m00s + [ 6/20] Upgrading libgomp-0:14.2.1-3.fc 100% | 170.8 MiB/s | 524.8 KiB | 00m00s + [ 7/20] Upgrading rpm-build-libs-0:4.19 100% | 15.6 MiB/s | 207.5 KiB | 00m00s + >>> Running pre-install scriptlet: rpm-0:4.19.94-1.fc41.x86_64 + >>> Stop pre-install scriptlet: rpm-0:4.19.94-1.fc41.x86_64 + [ 8/20] Upgrading rpm-0:4.19.94-1.fc41. 100% | 104.3 MiB/s | 2.5 MiB | 00m00s + [ 9/20] Upgrading openssl-libs-1:3.2.2- 100% | 190.9 MiB/s | 7.8 MiB | 00m00s + [10/20] Upgrading libstdc++-0:14.2.1-3. 100% | 145.6 MiB/s | 2.8 MiB | 00m00s + [11/20] Upgrading systemd-libs-0:256.6- 100% | 135.3 MiB/s | 2.0 MiB | 00m00s + [12/20] Erasing rpm-build-libs-0:4.19.9 100% | 1.0 KiB/s | 5.0 B | 00m00s + [13/20] Erasing libstdc++-0:14.2.1-1.fc 100% | 6.1 KiB/s | 31.0 B | 00m00s + [14/20] Erasing systemd-libs-0:256.5-1. 100% | 2.4 KiB/s | 20.0 B | 00m00s + [15/20] Erasing rpm-0:4.19.92-6.fc41.x8 100% | 26.7 KiB/s | 273.0 B | 00m00s + [16/20] Erasing rpm-libs-0:4.19.92-6.fc 100% | 2.4 KiB/s | 10.0 B | 00m00s + [17/20] Erasing openssl-libs-1:3.2.2-5. 100% | 9.5 KiB/s | 39.0 B | 00m00s + [18/20] Erasing zlib-ng-compat-0:2.1.7- 100% | 1.2 KiB/s | 5.0 B | 00m00s + [19/20] Erasing libgcc-0:14.2.1-1.fc41. 100% | 647.0 B/s | 11.0 B | 00m00s + >>> Running post-uninstall scriptlet: libgcc-0:14.2.1-1.fc41.x86_64 + >>> Stop post-uninstall scriptlet: libgcc-0:14.2.1-1.fc41.x86_64 + [20/20] Erasing libgomp-0:14.2.1-1.fc41 100% | 43.0 B/s | 9.0 B | 00m00s + >>> Running post-transaction scriptlet: rpm-0:4.19.94-1.fc41.x86_64 + >>> Stop post-transaction scriptlet: rpm-0:4.19.94-1.fc41.x86_64 + >>> Running trigger-install scriptlet: glibc-common-0:2.40-3.fc41.x86_64 + >>> Stop trigger-install scriptlet: glibc-common-0:2.40-3.fc41.x86_64 + >>> Running trigger-post-uninstall scriptlet: glibc-common-0:2.40-3.fc41.x86_64 + >>> Stop trigger-post-uninstall scriptlet: glibc-common-0:2.40-3.fc41.x86_64 + Complete! + Removed 24 files, 13 directories. 0 errors occurred. + ==> 8a9a360: IMAGE /tmp/velocity/build/fedora-41-8a9a360/8a9a360.sif (fedora@41) BUILT [0:00:21] + + ==> de9c02b: BUILD hello-world@1.0 ... + ==> de9c02b: COPYING FILES ... + FILE: /tmp/velocity/images/hello-world/files/hello_world.py -> /tmp/velocity/build/hello-world-1.0-de9c02b/hello_world.py + ==> de9c02b: GENERATING SCRIPT ... + SCRIPT: /tmp/velocity/build/hello-world-1.0-de9c02b/script + Bootstrap: localimage + From: /tmp/velocity/build/fedora-41-8a9a360/8a9a360.sif + + %files + hello_world.py /hello_world + + %post + dnf -y install python3 + chmod +x /hello_world + + %runscript + /hello_world + ==> de9c02b: BUILDING ... + #!/usr/bin/env bash + apptainer build --disable-cache /tmp/velocity/build/hello-world-1.0-de9c02b/de9c02b.sif /tmp/velocity/build/hello-world-1.0-de9c02b/script; + Updating and loading repositories: + Fedora 41 openh264 (From Cisco) - x86_ 100% | 11.1 KiB/s | 6.0 KiB | 00m01s + Fedora 41 - x86_64 100% | 11.7 MiB/s | 35.4 MiB | 00m03s + Fedora 41 - x86_64 - Updates 100% | 50.1 KiB/s | 31.9 KiB | 00m01s + Fedora 41 - x86_64 - Test Updates 100% | 1.8 MiB/s | 2.1 MiB | 00m01s + Repositories loaded. + Package Arch Version Repository Size + Installing: + python3 x86_64 3.13.0~rc2-1.fc41 fedora 31.8 KiB + Installing dependencies: + expat x86_64 2.6.3-1.fc41 updates-testing 291.5 KiB + libb2 x86_64 0.98.1-12.fc41 fedora 42.2 KiB + mpdecimal x86_64 2.5.1-16.fc41 fedora 204.9 KiB + python-pip-wheel noarch 24.2-1.fc41 fedora 1.2 MiB + python3-libs x86_64 3.13.0~rc2-1.fc41 fedora 40.3 MiB + Installing weak dependencies: + python-unversioned-command noarch 3.13.0~rc2-1.fc41 fedora 23.0 B + + Transaction Summary: + Installing: 7 packages + + Total size of inbound packages is 11 MiB. Need to download 11 MiB. + After this operation 42 MiB will be used (install 42 MiB, remove 0 B). + [1/7] libb2-0:0.98.1-12.fc41.x86_64 100% | 94.1 KiB/s | 25.7 KiB | 00m00s + [2/7] python3-0:3.13.0~rc2-1.fc41.x86_6 100% | 95.9 KiB/s | 27.4 KiB | 00m00s + [3/7] mpdecimal-0:2.5.1-16.fc41.x86_64 100% | 408.0 KiB/s | 89.0 KiB | 00m00s + [4/7] expat-0:2.6.3-1.fc41.x86_64 100% | 447.4 KiB/s | 114.1 KiB | 00m00s + [5/7] python-pip-wheel-0:24.2-1.fc41.no 100% | 2.5 MiB/s | 1.2 MiB | 00m00s + [6/7] python-unversioned-command-0:3.13 100% | 125.1 KiB/s | 10.5 KiB | 00m00s + [7/7] python3-libs-0:3.13.0~rc2-1.fc41. 100% | 8.1 MiB/s | 9.1 MiB | 00m01s + -------------------------------------------------------------------------------- + [7/7] Total 100% | 6.7 MiB/s | 10.6 MiB | 00m02s + Running transaction + [1/9] Verify package files 100% | 269.0 B/s | 7.0 B | 00m00s + [2/9] Prepare transaction 100% | 280.0 B/s | 7.0 B | 00m00s + [3/9] Installing expat-0:2.6.3-1.fc41.x 100% | 71.7 MiB/s | 293.6 KiB | 00m00s + [4/9] Installing python-pip-wheel-0:24. 100% | 310.4 MiB/s | 1.2 MiB | 00m00s + [5/9] Installing mpdecimal-0:2.5.1-16.f 100% | 50.3 MiB/s | 206.0 KiB | 00m00s + [6/9] Installing libb2-0:0.98.1-12.fc41 100% | 4.7 MiB/s | 43.3 KiB | 00m00s + [7/9] Installing python3-libs-0:3.13.0~ 100% | 139.9 MiB/s | 40.7 MiB | 00m00s + [8/9] Installing python3-0:3.13.0~rc2-1 100% | 10.9 MiB/s | 33.6 KiB | 00m00s + [9/9] Installing python-unversioned-com 100% | 1.8 KiB/s | 424.0 B | 00m00s + >>> Running trigger-install scriptlet: glibc-common-0:2.40-3.fc41.x86_64 + >>> Stop trigger-install scriptlet: glibc-common-0:2.40-3.fc41.x86_64 + Complete! + ==> de9c02b: IMAGE /tmp/velocity/build/hello-world-1.0-de9c02b/de9c02b.sif (hello-world@1.0) BUILT [0:00:14] + + ==> BUILT: /tmp/velocity/hello-world-1.0_fedora-41__x86_64-fedora.sif + Our hello-world image has been built! .. code-block:: bash - :emphasize-lines: 3 + :emphasize-lines: 7 - user@hostname:~$ podman image ls - REPOSITORY TAG IMAGE ID CREATED SIZE - localhost/hello-world__1.0__x86_64__fedora latest db958bad4f40 4 minutes ago 337 MB - docker.io/library/fedora 41 54d1373b70a2 5 weeks ago 180 MB + user@hostname:~$ ls + total 190972 + drwxr-xr-x 4 xjv users 4096 Sep 18 10:01 . + drwxrwxrwt 31 root root 131072 Sep 18 10:01 .. + drwxr-xr-x 4 xjv users 4096 Sep 18 10:01 build + -rwxr-xr-x 1 xjv users 66007040 Sep 18 09:42 fedora-38__x86_64-fedora.sif + -rwxr-xr-x 1 xjv users 129392640 Sep 18 10:01 hello-world-1.0_fedora-41__x86_64-fedora.sif + drwxr-xr-x 4 xjv users 4096 Sep 18 09:44 images + + +Now you can run the image! + +.. code-block:: bash + user@hostname:~$ apptainer run hello-world-1.0_fedora-41__x86_64-fedora.sif + Hello, World! diff --git a/src/velocity/__main__.py b/src/velocity/__main__.py index 6f27cae..f893db3 100644 --- a/src/velocity/__main__.py +++ b/src/velocity/__main__.py @@ -19,7 +19,7 @@ epilog="See https://github.com/olcf/velocity", ) parser.add_argument( - "-v", "--version", action="version", version=f"%(prog)s {version('velocity')}", help="program version" + "-v", "--version", action="version", version=f"%(prog)s {version('olcf-velocity')}", help="program version" ) parser.add_argument( "-D", From b4e46c11794b44709a78a6510ccdd45ec8515dac Mon Sep 17 00:00:00 2001 From: Asa <59259908+AcerP-py@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:51:36 -0400 Subject: [PATCH 10/10] Update README.md --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bf495b8..8b9a36f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

        VELOCITY

        +

        VELOCITY

        ## Description A container build manager. @@ -12,4 +12,7 @@ python3 -m http.server 8080 And then go to . ## Installation -Simply run the `setup-env.sh` script and set the correct variables. +``` commandline +pip install olcf-velocity +alias velocity="python3 -m velocity" +```