Blender 5.1.2 #1

Merged
stilobique merged 21 commits from Blender-5.1.2 into main 2026-07-31 10:57:39 +02:00
6 changed files with 124 additions and 88 deletions
Showing only changes of commit 062132487b - Show all commits
+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
Some log are generated inside a `logs` folder.
- `build`: all operation on build docker file.
+69 -26
View File
@@ -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:
+1 -30
View File
@@ -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()
print(release)
+2 -1
View File
@@ -1,2 +1,3 @@
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()