diff --git a/README.md b/README.md index c090df2..97e8186 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,24 @@ docker run --rm stilobique/blender:5.1.2 ./blender -b --python-expr "import bpy; ``` +# CLI helper tasks +Reusable helpers (`tasks.py`) are exposed as [invoke](https://www.pyinvoke.org/) tasks. + +```shell +# List available tasks +invoke clean + +# Clean Docker (all contexts: default, podman, ...) +python -m blender.cli clean + +# List Blender tags (Gitea) +python -m blender.cli get-tags-release + +# List stilobique/blender tags (Docker Hub) +python -m blender.cli get-tags-docker +``` + + # Logs Some log are generated inside a `logs` folder. - `build`: all operation on build docker file. diff --git a/blender/docker.py b/blender/docker.py index 126461f..291c7a8 100644 --- a/blender/docker.py +++ b/blender/docker.py @@ -1,12 +1,24 @@ import os +import re import docker import logging +import datetime from .version import BlenderVersion +from .dependency import BlenderDependency from pathlib import Path from docker.errors import BuildError, APIError, ContainerError, ImageNotFound +_ANSI_ESCAPE_RE = re.compile(r'\x1b\[[0-9;]*[a-zA-Z]') + + +def _clean_log_line(text: str) -> str: + """Strip ANSI escape codes and keep only the last update of a \r-rewritten line.""" + text = _ANSI_ESCAPE_RE.sub('', text) + return text.split('\r')[-1].strip() + + class BlenderDocker: def __init__(self, blender: BlenderVersion, severity: int = logging.WARNING): self._blender_version = blender @@ -21,10 +33,13 @@ class BlenderDocker: if not self._docker_file.exists(): print(f'Dockerfile not found: {self._docker_file}') exit(1) + + self._packages = BlenderDependency(blender) self._args = { 'b3d_vs_major': f'{self._blender_version.major}', 'b3d_vs_minor': f'{self._blender_version.minor}.{self._blender_version.hotfix}', + 'b3d_dependency': self._packages.__str__() } self._severity = severity @@ -66,39 +81,56 @@ class BlenderDocker: def build(self): """Build the Blender Docker Image""" - logger = self._get_logger('build') + time = datetime.datetime.now() + logger = self._get_logger(f'build-{time.strftime("%Y-%m-%d-%H-%M")}') + collected_logs = [] try: print('Building Blender Docker Image') logger.info('Building Blender Docker Image') logger.info(f'\tBuilds withs args: {self._args}') logger.info(f'\tTag generate: {self._docker_image_name}') - - self._docker_image, build_logs = self._docker_client.images.build( + + # Use the low-level API: images.build() only returns once the whole + # build is finished, so it gives no live progress. api.build() with + # decode=True streams each step (including cache hits) as it happens. + build_logs = self._docker_client.api.build( path=os.getcwd(), # nocache=True, buildargs=self._args, - tag=self._docker_image_name, - quiet=False, + tag=self._docker_image_name, rm=True, - forcerm=True + forcerm=True, + decode=True, ) - - # Debug log: + for chunk in build_logs: + collected_logs.append(chunk) + if 'stream' in chunk: - line = chunk['stream'].strip() - if line: - logger.debug(line) - + for raw_line in chunk['stream'].split('\n'): + line = _clean_log_line(raw_line) + if not line: + continue + if 'Using cache' in line: + line = f'[CACHE] {line}' + logger.info(line) + + elif 'status' in chunk: + status = _clean_log_line(chunk['status']) + progress = _clean_log_line(chunk.get('progress', '')) + line = f"{status} {chunk.get('id', '')} {progress}".strip() + if 'Already exists' in status: + line = f'[CACHE] {line}' + logger.info(line) + elif 'error' in chunk: logger.error(chunk['error']) - - + raise BuildError(chunk['error'], collected_logs) + + self._docker_image = self._docker_client.images.get(self._docker_image_name) + except BuildError as e: logger.exception(f'Build docker image failed: {e}') - for line in e.build_log: - if 'stream' in line: - logger.debug(line['stream'].strip()) raise @staticmethod @@ -108,20 +140,23 @@ class BlenderDocker: def test(self) -> bool: """ - This method request an image (call the method build before) and try to execute the blender inside the container + This method request an image (call the method build before) and try to execute the blender inside the container to check if the program can be correctly executed. Based on `docker run --rm stilobique/blender:5.1.2 ./blender --version` :return: True if no issue are returned """ logger = self._get_logger('test') + container_name = f"Blender-Docker-Testing-{self._blender_version.tag_name_slugify()}" + command = ['./blender', '--version'] try: print(f'Testing your Blender Docker image {self._docker_image_name}') - logger.info(f'Testing your Blender Docker image {self._docker_image_name}.') + logger.info(f'Testing image "{self._docker_image_name}" in container "{container_name}" ' + f'with command: {" ".join(command)}') test = self._docker_client.containers.run( - name=f"Blender-Docker-Testing-{self._blender_version.tag_name_slugify()}", + name=container_name, image=self._docker_image_name, - command=['./blender', '--version'], + command=command, auto_remove=True, remove=True, detach=True, @@ -132,24 +167,32 @@ class BlenderDocker: result = test.wait() exit_code = result.get('StatusCode') - logger.info(f'Exit code return: {exit_code}') - logger.info(test.logs(stdout=True, stderr=True).decode('utf-8', errors='replace').strip()) + container_logs = test.logs(stdout=True, stderr=True).decode('utf-8', errors='replace').strip() + for raw_line in container_logs.split('\n'): + line = _clean_log_line(raw_line) + if line: + logger.info(f'[container] {line}') if exit_code == 0: + logger.info(f'Test succeeded (exit code {exit_code}) for image "{self._docker_image_name}".') return True + logger.error(f'Test failed (exit code {exit_code}) for image "{self._docker_image_name}".') print(f'Testing your Blender Docker image {self._docker_image_name} failed.') return False - + except ContainerError as e: + logger.exception(f'Container error while testing "{self._docker_image_name}": {e}') print(f'Container error: {e}') exit(1) - + except ImageNotFound as e: + logger.exception(f'Image "{self._docker_image_name}" not found: {e}') print(f'Image not found: {e}') exit(1) - + except APIError as e: + logger.exception(f'API error while testing "{self._docker_image_name}": {e}') print(f'API error: {e}') for line in e.build_log: if 'stream' in line: diff --git a/blender/misc.py b/blender/misc.py index c56d534..abc401e 100644 --- a/blender/misc.py +++ b/blender/misc.py @@ -2,7 +2,6 @@ import logging import os import json import requests -import argparse from pathlib import Path from subprocess import run, CalledProcessError @@ -90,32 +89,4 @@ def get_all_tags_from_docker(): # All tags data: for release in j: - print(release) - - -ACTIONS = { - 'clean': clean_all_dockers, - 'get-tags-release': get_all_tags_from_blender, - 'get-tags-docker': get_all_tags_from_docker, -} - - -def main(): - # Todo Use invoke ? - """ - Call function from the command line - Ex: python ./blender/misc.py get-tags-release - :return: - """ - parser = argparse.ArgumentParser(description='Blender docker helper') - parser.add_argument('action', - choices=ACTIONS.keys(), - help='Action selected' - ) - args = parser.parse_args() - - ACTIONS[args.action]() - - -if __name__ == '__main__': - main() \ No newline at end of file + print(release) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f7a1595..827604f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ docker==7.2.0 -requests==2.34.2 \ No newline at end of file +requests==2.34.2 +invoke==3.0.3 \ No newline at end of file diff --git a/scripts/gen_packages_txt.py b/scripts/gen_packages_txt.py deleted file mode 100644 index cb25a07..0000000 --- a/scripts/gen_packages_txt.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python3 -""" -Lit install_linux_packages.py et génère un fichier .txt avec les paquets -Debian/Ubuntu à installer. -""" -import re -import sys - -READER="install_linux_packages.py" -OUT="packages.txt" - - -def main(): - with open(READER, "r", encoding="utf-8") as f: - content = f.read() - - # Cherche toutes les lignes du type: DISTRO_ID_DEBIAN: "nom-du-paquet" - packages = re.findall(r'DISTRO_ID_DEBIAN:\s*"([^"]+)"', content) - - # Enlève les doublons, trie - packages = sorted(set(packages)) - - with open(OUT, "w", encoding="utf-8") as f: - for pkg in packages: - f.write(pkg + "\n") - - print(f"{len(packages)} paquets écrits dans {OUT}") - - -if __name__ == "__main__": - main() diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..069b700 --- /dev/null +++ b/tasks.py @@ -0,0 +1,34 @@ +""" +Invoke CLI entry point for the reusable helpers in `blender/misc.py`. +Ex: python -m blender.cli clean +Ex: python -m blender.cli get-tags-release +""" + +from invoke import Collection, Program, task + +from blender.misc import clean_all_dockers, get_all_tags_from_blender, get_all_tags_from_docker + + +@task(name='clean') +def clean(ctx): + """Nettoie Docker pour tous les contextes disponibles (default, podman, ...).""" + clean_all_dockers() + + +@task(name='get-tags-release') +def get_tags_release(ctx): + """Recupere tous les tags de release Blender depuis Gitea.""" + get_all_tags_from_blender() + + +@task(name='get-tags-docker') +def get_tags_docker(ctx): + """Recupere tous les tags de l'image Docker stilobique/blender depuis Docker Hub.""" + get_all_tags_from_docker() + + +namespace = Collection(clean, get_tags_release, get_tags_docker) +program = Program(namespace=namespace, version='0.1.0') + +if __name__ == '__main__': + program.run() \ No newline at end of file