Make a basic setup with invoke.

Fix log information.
Remove unused script file.
This commit is contained in:
2026-07-24 09:53:36 +02:00
parent bb3de8fd4e
commit 062132487b
6 changed files with 124 additions and 88 deletions
+18
View File
@@ -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 # Logs
Some log are generated inside a `logs` folder. Some log are generated inside a `logs` folder.
- `build`: all operation on build docker file. - `build`: all operation on build docker file.
+69 -26
View File
@@ -1,12 +1,24 @@
import os import os
import re
import docker import docker
import logging import logging
import datetime
from .version import BlenderVersion from .version import BlenderVersion
from .dependency import BlenderDependency
from pathlib import Path from pathlib import Path
from docker.errors import BuildError, APIError, ContainerError, ImageNotFound 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: class BlenderDocker:
def __init__(self, blender: BlenderVersion, severity: int = logging.WARNING): def __init__(self, blender: BlenderVersion, severity: int = logging.WARNING):
self._blender_version = blender self._blender_version = blender
@@ -21,10 +33,13 @@ class BlenderDocker:
if not self._docker_file.exists(): if not self._docker_file.exists():
print(f'Dockerfile not found: {self._docker_file}') print(f'Dockerfile not found: {self._docker_file}')
exit(1) exit(1)
self._packages = BlenderDependency(blender)
self._args = { self._args = {
'b3d_vs_major': f'{self._blender_version.major}', 'b3d_vs_major': f'{self._blender_version.major}',
'b3d_vs_minor': f'{self._blender_version.minor}.{self._blender_version.hotfix}', 'b3d_vs_minor': f'{self._blender_version.minor}.{self._blender_version.hotfix}',
'b3d_dependency': self._packages.__str__()
} }
self._severity = severity self._severity = severity
@@ -66,39 +81,56 @@ class BlenderDocker:
def build(self): def build(self):
"""Build the Blender Docker Image""" """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: try:
print('Building Blender Docker Image') print('Building Blender Docker Image')
logger.info('Building Blender Docker Image') logger.info('Building Blender Docker Image')
logger.info(f'\tBuilds withs args: {self._args}') logger.info(f'\tBuilds withs args: {self._args}')
logger.info(f'\tTag generate: {self._docker_image_name}') 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(), path=os.getcwd(),
# nocache=True, # nocache=True,
buildargs=self._args, buildargs=self._args,
tag=self._docker_image_name, tag=self._docker_image_name,
quiet=False,
rm=True, rm=True,
forcerm=True forcerm=True,
decode=True,
) )
# Debug log:
for chunk in build_logs: for chunk in build_logs:
collected_logs.append(chunk)
if 'stream' in chunk: if 'stream' in chunk:
line = chunk['stream'].strip() for raw_line in chunk['stream'].split('\n'):
if line: line = _clean_log_line(raw_line)
logger.debug(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: elif 'error' in chunk:
logger.error(chunk['error']) 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: except BuildError as e:
logger.exception(f'Build docker image failed: {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 raise
@staticmethod @staticmethod
@@ -108,20 +140,23 @@ class BlenderDocker:
def test(self) -> bool: 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. to check if the program can be correctly executed.
Based on `docker run --rm stilobique/blender:5.1.2 ./blender --version` Based on `docker run --rm stilobique/blender:5.1.2 ./blender --version`
:return: True if no issue are returned :return: True if no issue are returned
""" """
logger = self._get_logger('test') logger = self._get_logger('test')
container_name = f"Blender-Docker-Testing-{self._blender_version.tag_name_slugify()}"
command = ['./blender', '--version']
try: try:
print(f'Testing your Blender Docker image {self._docker_image_name}') 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( 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, image=self._docker_image_name,
command=['./blender', '--version'], command=command,
auto_remove=True, auto_remove=True,
remove=True, remove=True,
detach=True, detach=True,
@@ -132,24 +167,32 @@ class BlenderDocker:
result = test.wait() result = test.wait()
exit_code = result.get('StatusCode') exit_code = result.get('StatusCode')
logger.info(f'Exit code return: {exit_code}') container_logs = test.logs(stdout=True, stderr=True).decode('utf-8', errors='replace').strip()
logger.info(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: if exit_code == 0:
logger.info(f'Test succeeded (exit code {exit_code}) for image "{self._docker_image_name}".')
return True 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.') print(f'Testing your Blender Docker image {self._docker_image_name} failed.')
return False return False
except ContainerError as e: except ContainerError as e:
logger.exception(f'Container error while testing "{self._docker_image_name}": {e}')
print(f'Container error: {e}') print(f'Container error: {e}')
exit(1) exit(1)
except ImageNotFound as e: except ImageNotFound as e:
logger.exception(f'Image "{self._docker_image_name}" not found: {e}')
print(f'Image not found: {e}') print(f'Image not found: {e}')
exit(1) exit(1)
except APIError as e: except APIError as e:
logger.exception(f'API error while testing "{self._docker_image_name}": {e}')
print(f'API error: {e}') print(f'API error: {e}')
for line in e.build_log: for line in e.build_log:
if 'stream' in line: if 'stream' in line:
+1 -30
View File
@@ -2,7 +2,6 @@ import logging
import os import os
import json import json
import requests import requests
import argparse
from pathlib import Path from pathlib import Path
from subprocess import run, CalledProcessError from subprocess import run, CalledProcessError
@@ -90,32 +89,4 @@ def get_all_tags_from_docker():
# All tags data: # All tags data:
for release in j: for release in j:
print(release) 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()
+2 -1
View File
@@ -1,2 +1,3 @@
docker==7.2.0 docker==7.2.0
requests==2.34.2 requests==2.34.2
invoke==3.0.3
-31
View File
@@ -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()
+34
View File
@@ -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()