Blender 5.1.2 #1
@@ -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.
|
||||||
|
|||||||
+59
-16
@@ -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
|
||||||
@@ -22,9 +34,12 @@ class BlenderDocker:
|
|||||||
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
|
||||||
@@ -114,14 +146,17 @@ class BlenderDocker:
|
|||||||
: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:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -91,31 +90,3 @@ 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()
|
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
docker==7.2.0
|
docker==7.2.0
|
||||||
requests==2.34.2
|
requests==2.34.2
|
||||||
|
invoke==3.0.3
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user