10 Commits
Author SHA1 Message Date
stilobique 693afc76fd Setup python builder 2026-07-18 16:45:19 +02:00
stilobique 82653b4fba Update dependencies get 2026-07-18 11:28:16 +02:00
stilobique dc2dc84f4f Add Jetbrains project icon 2026-07-17 08:45:55 +02:00
stilobique db823ebe3f Fix installation with blender 5.2.0 2026-07-16 16:36:23 +02:00
stilobique 1ad119e7d2 Clear old files 2026-06-25 16:02:17 +02:00
stilobique 816e8f5c12 Update readme file, and clear docker file 2026-06-25 13:52:02 +02:00
stilobique 39b44c9a08 REfactoring to work with blender 5.1.x 2026-06-25 13:42:25 +02:00
stilobique 029a7c3b38 Fix build and documente check info 2026-06-19 18:02:24 +02:00
stilobique 5ed95e455c Fix image build 2026-06-19 16:58:32 +02:00
stilobique 204c1da235 Fix the build with latest release 2026-06-19 16:39:09 +02:00
14 changed files with 125 additions and 667 deletions
-6
View File
@@ -1,9 +1,3 @@
### Logs
**/logs/**
### Config/setup """
*.env
### PyCharm ### ### PyCharm ###
.idea/ .idea/
-27
View File
@@ -1,27 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Automation Builder" type="PythonConfigurationType" factoryName="Python">
<module name="rider.module" />
<option name="ENV_FILES" value="$PROJECT_DIR$/config.env" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="SDK_NAME" value="Python 3.12 (BlenderDocker)" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="DEBUG_JUST_MY_CODE" value="false" />
<option name="RUN_TOOL" value="" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/main.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
</component>
+1 -1
View File
@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Build BlenderDocker" type="docker-deploy" factoryName="dockerfile" server-name="Docker"> <configuration default="false" name="Build BlenderDocker" type="docker-deploy" factoryName="dockerfile" server-name="Docker Desktop">
<deployment type="dockerfile"> <deployment type="dockerfile">
<settings> <settings>
<option name="imageTag" value="stilobique/blender:4.5.11" /> <option name="imageTag" value="stilobique/blender:4.5.11" />
-27
View File
@@ -1,27 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Clean docker Images " type="PythonConfigurationType" factoryName="Python">
<module name="rider.module" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="SDK_NAME" value="Python 3.12 (BlenderDocker)" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="DEBUG_JUST_MY_CODE" value="false" />
<option name="RUN_TOOL" value="" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/blender/misc.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
</component>
+17 -71
View File
@@ -1,11 +1,4 @@
# Pinned to the exact Python version Blender's own CMake requires for this FROM python:latest AS b3dock
# release (build_files/cmake/Modules/FindPythonLibsUnix.cmake,
# _PYTHON_VERSION_SUPPORTED) - a floating `python` tag drifts to whatever
# "latest" currently is, which stops matching once it moves past what an
# older/LTS Blender release hard-pins (it has no matching apt package to
# install after the fact, unlike a missing library).
ARG python_version=3.13
FROM python:${python_version} AS b3dock
# Setup all software version request # Setup all software version request
ARG b3d_vs_major=5.2 ARG b3d_vs_major=5.2
@@ -13,83 +6,36 @@ ARG b3d_vs_minor=0
LABEL Author="stilobique" LABEL Author="stilobique"
LABEL Title="Blender Docker for Unit Test" LABEL Title="Blender Docker for Unit Test"
LABEL Maintainer="Aurelien Vaillant contact@aurelien-vaillant.net" LABEL maintainer="Aurelien Vaillant contact@aurelien-vaillant.net"
#ENV TZ=Europe/Paris
#RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
## 01. Start First stage to build blender ## 01. Start First stage to build blender
# Install dependencies # Install dependencies
RUN apt-get update && apt-get install -y git git-lfs sudo RUN apt-get update && apt-get install -y git git-lfs
# Get source code # Compile Blender
RUN git clone --depth 1 --branch v${b3d_vs_major}.${b3d_vs_minor} \ RUN git clone --depth 1 --branch v${b3d_vs_major}.${b3d_vs_minor} \
https://projects.blender.org/blender/blender.git /opt/blender https://projects.blender.org/blender/blender.git /opt/blender
# Start build # Get dependencies
# install_linux_packages.py (default, no --all) skips a handful of packages COPY ./scripts/gen_packages_txt.py /opt/blender/build_files/build_environment/gen_packages_txt.py
# that are only provided via the precompiled-libs bundle or via --all:
# libepoxy-dev, Boost (all components), OpenImageIO. --all itself is not an
# option here: it also checks its own apt catalog for a python3-dev matching
# [3.11, 3.13[, which fails on Debian trixie regardless of which Python the
# base image runs (that check queries apt's package catalog, not the actual
# interpreter on PATH). Since `make update` below skips the precompiled
# bundle on Linux by default, these are needed from apt regardless - install
# them explicitly instead.
RUN cd /opt/blender/build_files/build_environment && \ RUN cd /opt/blender/build_files/build_environment && \
chmod a+x ./install_linux_packages.py && \ python gen_packages_txt.py
python install_linux_packages.py && \ RUN cd /opt/blender/build_files/build_environment && chmod a+x ./install_linux_packages.py && \
apt-get install -y libepoxy-dev libboost-all-dev libopenimageio-dev libembree-dev libpugixml-dev && \ python install_linux_packages.py
pip install numpy cython && \
cd /opt/blender && make update && make -j$(nproc)
# Discover Blender's *actual* runtime shared-library needs from the compiled RUN cd /opt/blender && make update
# binary itself (ldd), instead of guessing from install_linux_packages.py RUN cd /opt/blender && make -j$(nproc)
# (that script only covers build-time deps; libSM/libICE were never listed
# there but the binary needs them at runtime). Every resolved .so is mapped
# back to the apt package that owns it, so stage 02 can install exactly that.
RUN ldd /opt/build_linux/bin/blender | tee /dev/stderr | grep 'not found' && \
(echo 'Unresolved libs in the build stage itself, investigate first' && exit 1) || true
# `realpath` is required: /lib is a merged-usr symlink to /usr/lib, and dpkg's
# file database is keyed on the canonical /usr/lib/... path (with the full
# soname suffix, e.g. libX11.so.6.4.0) — querying the /lib/....so.6 symlink
# path directly returns "no path found matching pattern".
# These names are resolved against Debian (this stage's distro), but stage 02
# installs them on Ubuntu - the two occasionally disagree on a package name
# for the same library, and not just cosmetically: Debian's `libjpeg62-turbo`
# provides libjpeg.so.62, but Ubuntu's identically-ABI-sounding
# `libjpeg-turbo8` actually provides a DIFFERENT soname (libjpeg.so.8) -
# Ubuntu ships the old libjpeg.so.62 ABI as a separate `libjpeg62` package.
# Debian's generic `libxml2` is Ubuntu's soname-suffixed `libxml2-16`.
# Remap the known cases here rather than in stage 02.
RUN ldd /opt/build_linux/bin/blender \
| awk '{print $3}' | grep '^/' | sort -u \
| xargs -r realpath | sort -u \
| xargs -r dpkg -S 2>/dev/null | cut -d: -f1 | tr ',' '\n' | sed 's/^ //' | sort -u \
| sed -e 's/^libjpeg62-turbo$/libjpeg62/' -e 's/^libxml2$/libxml2-16/' \
| tee /opt/build_linux/runtime_packages.txt
## 02. Build optimissed image ## 02. Build optimissed image
## Setup a Multistage optimisation ## Setup a Multistage optimisation
FROM ubuntu:25.10 AS final FROM ubuntu:25.04 AS final
# Re-declared: ARGs before the first FROM only apply to that FROM line, not COPY --from=b3dock /opt/blender/build_files/build_environment/packages.txt /tmp/packages.txt
# to instructions in later stages. RUN apt-get update && xargs -a /tmp/packages.txt apt-get install -y
ARG python_version=3.13
# Install exactly the runtime packages ldd found the compiled binary needs
# (see the b3dock stage above) instead of guessing from Blender's build-time
# package list. Ubuntu and Debian share package names for these core libs.
COPY --from=b3dock /opt/build_linux/runtime_packages.txt /tmp/runtime_packages.txt
RUN apt-get update && \
xargs -a /tmp/runtime_packages.txt apt-get install -y --no-install-recommends && \
rm -rf /tmp/runtime_packages.txt /var/lib/apt/lists/*
# libpython<version>.so isn't an apt package: the b3dock base image builds
# Python from source into /usr/local, so ldd/dpkg-S (above) can never find an
# owning package for it - Ubuntu's apt only ships whatever Python versions it
# currently defaults to, which won't match an older/LTS Blender's pin. Copy
# the .so Blender actually linked against directly from the build stage.
COPY --from=b3dock /usr/local/lib/libpython${python_version}.so.1.0 /usr/local/lib/libpython${python_version}.so.1.0
RUN ldconfig
RUN useradd -m -s /bin/bash bld RUN useradd -m -s /bin/bash bld
+1 -46
View File
@@ -5,33 +5,7 @@
This repository is an automated blender docker image file generated, it's usefully with blender addon unit test. This repository is an automated blender docker image file generated, it's usefully with blender addon unit test.
# Building # Adding an Addon
## Docker Hub credentials
`main.py` logs into Docker Hub before building, so it needs `DOCKER_HUB_USER` and `DOCKER_HUB_PASSWORD` in the environment.
Create a `config.env` file at the repo root (gitignored):
```shell
DOCKER_HUB_USER=your_docker_hub_username
DOCKER_HUB_PASSWORD=your_docker_hub_password
```
It's loaded automatically by the `Automation Builder` run configuration (`ENV_FILES`) when running `main.py` from the IDE. Running it another way? Export those two variables yourself first.
## Start your build
You can use Invoke to easily start a build, a clean or more.
```shell
invoke --list
```
## Debug and logs
Some log are generated inside a `logs` folder.
- `build`: all operation on build docker file.
- `clean`: from the dedicated function to clean all dockers images and cache.
- `test`: simple class method to try a specific docker image.
## Check if the build work
# Use case
## Adding an Addon
Mount a new volume with your addon inside a folder (ex `/blender-plugin/plugin-archive.zip`), when you run your docker container add this volume and execute a script to add-it with the blender installed. Mount a new volume with your addon inside a folder (ex `/blender-plugin/plugin-archive.zip`), when you run your docker container add this volume and execute a script to add-it with the blender installed.
Python function to install an addon with Blender. The file his named `install.py` Python function to install an addon with Blender. The file his named `install.py`
@@ -66,22 +40,3 @@ docker run --rm --entrypoint /bin/bash stilobique/blender:5.1.2 -c 'ldd /opt/ble
# Test fonctionnel # Test fonctionnel
docker run --rm stilobique/blender:5.1.2 ./blender -b --python-expr "import bpy; print('OK:', bpy.app.version_string)" docker run --rm stilobique/blender:5.1.2 ./blender -b --python-expr "import bpy; print('OK:', bpy.app.version_string)"
``` ```
# 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
```
-83
View File
@@ -1,83 +0,0 @@
import os
import requests
import tempfile
import importlib.util
from blender.version import BlenderVersion
# "Wayland Protocols" has no "Library" in its name but is still a
# build-critical dependency, so it's matched by name too.
_EXTRA_PACKAGE_NAMES = {'wayland protocols'}
class BlenderDependency:
def __init__(self, blender: BlenderVersion):
self._blender_version = blender
self._url = rf'https://projects.blender.org/api/v1/repos/blender/blender/raw/build_files/build_environment/install_linux_packages.py?ref=v{self._blender_version.tag_name()}'
self.module = self._get_source_file()
self.packages = list()
self._get_list()
self.packages_complete = list()
self._get_complete_list()
def _get_source_file(self):
"""Download the Blender dependency script and import it at runtime."""
r = requests.get(self._url)
r.raise_for_status()
with tempfile.NamedTemporaryFile(mode='wb', suffix='.py', delete=False) as t:
t.write(r.content)
path = t.name
try:
spec = importlib.util.spec_from_file_location('install_linux_packages', path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
finally:
os.remove(path)
def _get_list(self):
"""Keep the mandatory 'library' packages (and Wayland Protocols) as Debian/Ubuntu apt names."""
sources = self.module.BUILD_MANDATORY_SUBPACKAGES
if hasattr(self.module, 'DEPS_CRITICAL_SUBPACKAGES'):
sources += self.module.DEPS_CRITICAL_SUBPACKAGES
for package in self._flatten(sources):
if 'library' not in package.name.lower() and package.name.lower() not in _EXTRA_PACKAGE_NAMES:
continue
name = package.distro_package_names.get(self.module.DISTRO_ID_DEBIAN)
if isinstance(name, str):
self.packages.append(name)
def _get_complete_list(self):
"""All packages required to build Blender (PACKAGES_BASICS_BUILD) as Debian/Ubuntu apt names."""
if hasattr(self.module, 'PACKAGES_BASICS_BUILD'):
for package in self._flatten(self.module.PACKAGES_BASICS_BUILD):
name = package.distro_package_names.get(self.module.DISTRO_ID_DEBIAN)
if isinstance(name, str):
self.packages_complete.append(name)
def _flatten(self, packages):
"""Yield every leaf Package, descending into is_group sub_packages."""
for package in packages:
if package.is_group:
yield from self._flatten(package.sub_packages)
else:
yield package
def __str__(self):
return ' '.join(self.packages)
def complete_str(self):
return ' '.join(self.packages_complete)
if __name__ == "__main__":
bld = BlenderVersion(version='5.0.0')
dep = BlenderDependency(blender=bld)
print('Show All packages:')
print(dep.__str__())
+26 -227
View File
@@ -1,153 +1,49 @@
import os import os
import re
import docker import docker
import logging
import datetime
import requests
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
_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):
self._blender_version = blender self._blender_version = blender
self._docker_client = docker.from_env() # Todo get a user config
self.login() # self._docker_client = docker.DockerClient(base_url=f'unix://{Path.home()}/.docker/desktop/docker.sock')
self._docker_client = docker.DockerClient(base_url=f'unix:///var/run/docker.sock')
self._docker_image = None self._docker_image = None
self._repository = 'stilobique/blender'
self._docker_image_name = self._blender_version.format_to_tag(self._repository)
self._docker_file = Path(os.getcwd(), 'Dockerfile') self._docker_file = Path(os.getcwd(), 'Dockerfile')
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__(),
'python_version': self._get_required_python_version(),
} }
self._severity = severity
def _get_required_python_version(self) -> str:
"""
Fetch the exact Python version (major.minor) Blender's own CMake pins for this
release, so the build stage's base image can match it instead of drifting with
whatever a floating `python` tag currently resolves to.
"""
url = (f'https://projects.blender.org/api/v1/repos/blender/blender/raw/'
f'build_files/cmake/Modules/FindPythonLibsUnix.cmake?ref=v{self._blender_version.tag_name()}')
r = requests.get(url)
r.raise_for_status()
match = re.search(r'set\(_PYTHON_VERSION_SUPPORTED\s+([0-9]+\.[0-9]+)\)', r.text)
if not match:
print(f'Could not determine required Python version from {url}')
exit(1)
return match.group(1)
def _get_logger(self, name: str) -> logging.Logger:
"""
Private Method to set a logger for a specific method set (by his name)
:param name: Make the logger file name.
:return:
"""
log_dir = Path('logs')
log_dir.mkdir(parents=True, exist_ok=True)
logger = logging.getLogger(f'blender_docker.{name}')
logger.setLevel(self._severity)
logger.propagate = False
if not logger.handlers:
handler = logging.FileHandler(log_dir / f'{name}.log', encoding='utf-8')
handler.setFormatter(logging.Formatter(
'%(asctime)s | [%(levelname)s] %(message)s'
))
logger.addHandler(handler)
return logger
def login(self):
"""Login to the Docker Hub"""
try:
print('Login to Hub Docker.')
self._docker_client.login(
username=os.environ['DOCKER_HUB_USER'],
password=os.environ['DOCKER_HUB_PASSWORD']
)
except APIError as e:
print(f'Login failed with error: {e}')
exit(1)
def build(self): def build(self):
"""Build the Blender Docker Image"""
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('\tBuilding Blender Docker')
logger.info('Building Blender Docker Image') print(f'\tWhere to build: {os.getcwd()}')
logger.info(f'\tBuilds withs args: {self._args}') print(f'\tArgs: {self.args}')
logger.info(f'\tTag generate: {self._docker_image_name}') print(f'\tTage generated: {self._blender_version.format_to_tag()}')
build = 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, buildargs=self.args,
buildargs=self._args, tag=self._blender_version.format_to_tag(),
tag=self._docker_image_name, quiet=False,
rm=True,
forcerm=True,
decode=True,
) )
for chunk in build_logs: print(f'\tBuilding Blender Docker Image done: {build}')
collected_logs.append(chunk) return build
if 'stream' in chunk:
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: except BuildError as e:
logger.exception(f'Build docker image failed: {e}') print("Hey something went wrong with image build!")
for line in e.build_log:
if 'stream' in line:
print(line['stream'].strip())
raise raise
@staticmethod @staticmethod
@@ -155,110 +51,13 @@ class BlenderDocker:
print('\tSet Tags version and/or Latest') print('\tSet Tags version and/or Latest')
pass pass
def test(self) -> bool: @staticmethod
""" def test(self):
This method request an image (call the method build before) and try to execute the blender inside the container print('\tTesting Blender Docker')
to check if the program can be correctly executed. # 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
"""
logger = self._get_logger('test')
container_name = f"Blender-Docker-Testing-{self._blender_version.tag_name_slugify()}"
command = ['./blender', '--version']
container = None
try:
print(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)}')
# auto_remove is intentionally NOT used: Docker can remove the container
# right after it exits, racing with the logs() call below and turning a
# real container-side error into an unrelated 409 "dead or marked for
# removal" API error. We remove the container ourselves once logs are read.
container = self._docker_client.containers.run(
name=container_name,
image=self._docker_image_name,
command=command,
detach=True,
stdout=True,
stderr=True,
)
result = container.wait()
exit_code = result.get('StatusCode')
container_logs = container.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}')
exit(1)
finally:
if container is not None:
try:
container.remove(force=True)
except APIError:
pass pass
def push(self): def push(self):
"""Push the Blender Docker Image""" # TODO Login to hub
time = datetime.datetime.now() pass
logger = self._get_logger(f'push')
collected_logs = []
try:
print(f'Pushing Blender Docker image {self._docker_image_name}')
logger.info(f'Pushing Blender Docker Image')
logger.info(f'\tTag pushed: {self._docker_image_name}')
# Use the low-level API: images.push() only returns once the whole
# push is finished, so it gives no live progress. api.push() with
# decode=True streams each layer's progress as it happens.
push_logs = self._docker_client.api.push(
repository=self._repository,
tag=self._blender_version.tag_name(),
stream=True,
decode=True,
)
for chunk in push_logs:
collected_logs.append(chunk)
if '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 'Layer already exists' in status:
line = f'[CACHE] {line}'
logger.info(line)
elif 'error' in chunk:
logger.error(chunk['error'])
raise APIError(chunk['error'])
logger.info(f'Push succeeded for image "{self._docker_image_name}".')
except APIError as e:
logger.exception(f'Push docker image failed: {e}')
raise
-98
View File
@@ -1,98 +0,0 @@
import logging
import os
import json
import requests
from pathlib import Path
from subprocess import run, CalledProcessError
def clean_all_dockers():
"""
Nettoie Docker pour tous les contextes disponibles (default, podman, ...).
Equivalent a, pour chaque contexte :
docker context use <context>
docker system prune -a -f
Puis remet le contexte d'origine en place a la fin.
"""
logs_dir = Path(os.getcwd(), 'logs')
logs_dir.mkdir(parents=True, exist_ok=True)
logger = logging.getLogger('blender_docker.clean')
logger.setLevel(logging.DEBUG)
logger.propagate = False
if not logger.handlers:
handler = logging.FileHandler(logs_dir / 'clean.log')
handler.setFormatter(logging.Formatter(
'%(asctime)s|%(name)s [%(levelname)s] %(message)s'
))
logger.addHandler(handler)
# Contexte courant, pour le restaurer a la fin
original_context = run(
['docker', 'context', 'show'],
capture_output=True, text=True, check=True
).stdout.strip()
logger.info(f'Contexte d\'origine: {original_context}')
# Liste de tous les contextes disponibles (format json, un objet par ligne)
contexts_raw = run(
['docker', 'context', 'ls', '--format', '{{json .}}'],
capture_output=True, text=True, check=True
).stdout.strip().splitlines()
contexts = [json.loads(line)['Name'] for line in contexts_raw if line]
print(f'\tContextes trouves: {contexts}')
logger.info(f'Contextes trouves: {contexts}')
for context in contexts:
try:
print(f'\tNettoyage du contexte: {context}')
logger.info(f'Nettoyage du contexte: {context}')
run(['docker', 'context', 'use', context], check=True)
result = run(
['docker', 'system', 'prune', '-a', '-f'],
capture_output=True, text=True, check=True
)
print(f'\t{result.stdout.strip()}')
logger.info(result.stdout.strip())
except CalledProcessError as e:
print(f'\tErreur sur le contexte {context}: {e.stderr.strip()}')
logger.error(f'Erreur sur le contexte {context}: {e.stderr.strip()}')
# On remet le contexte d'origine
run(['docker', 'context', 'use', original_context], check=True)
print(f'\tContexte restaure: {original_context}')
logger.info(f'Contexte restaure: {original_context}')
def get_all_tags_from_blender():
"""From the Gitea api, get all blender tags release"""
r = requests.get(r"https://projects.blender.org/api/v1/repos/blender/blender/tags")
print(r.text)
j = r.json()
# All tags data:
for release in j:
print(release)
def get_all_tags_from_docker():
"""From the Gitea api, get all blender tags release"""
r = requests.get(r"https://hub.docker.com/v2/namespaces/stilobique/repositories/blender/tags")
print(r.text)
j = r.json()
# All tags data:
for release in j:
print(release)
if __name__ == '__main__':
print('Clean docker')
clean_all_dockers()
print('Done')
+1 -18
View File
@@ -1,4 +1,3 @@
import re
class BlenderVersion: class BlenderVersion:
@@ -7,7 +6,6 @@ class BlenderVersion:
Need an object like 'X.Y.Z' to work correctly, build a pythonic object about a blender version. Need an object like 'X.Y.Z' to work correctly, build a pythonic object about a blender version.
:param version: :param version:
""" """
self._check_pattern(version)
self.major : str = '' self.major : str = ''
self.minor : str = '' self.minor : str = ''
self.hotfix : str = '' self.hotfix : str = ''
@@ -20,24 +18,9 @@ class BlenderVersion:
self.minor = version[1] self.minor = version[1]
self.hotfix = version[2] self.hotfix = version[2]
def format_to_tag(self, repository: str) -> str: def format_to_tag(self) -> str:
"""From version information, format to return a string""" """From version information, format to return a string"""
return f'{repository}:{self.major}.{self.minor}.{self.hotfix}'
def tag_name(self) -> str:
"""Return the tag name 'latest', 'X.Y.Z'..."""
return f'{self.major}.{self.minor}.{self.hotfix}' return f'{self.major}.{self.minor}.{self.hotfix}'
def tag_name_slugify(self):
return f'{self.major}-{self.minor}-{self.hotfix}'
@staticmethod
def _check_pattern(tested: str):
"""Simple test to look if the string give can be a blender version name."""
test = re.match(r'^[0-9]{1}.[0-9]+.[0-9]+$', tested)
if test is None:
print(f'Bad version name set: {tested}')
exit(1)
def __str__(self): def __str__(self):
return str(self.major + '.' + self.minor + '.' + self.hotfix) return str(self.major + '.' + self.minor + '.' + self.hotfix)
+34 -13
View File
@@ -1,24 +1,45 @@
from logging import INFO import os
from blender.version import BlenderVersion from blender.version import BlenderVersion
from blender.docker import BlenderDocker from blender.docker import BlenderDocker
# Todo Update readme file badge with blender version # TODO Update readme file badge with blender version
# TODO add a check if the image build are stored in the docker register
def build_docker(b3d_version: (str, str), latest: bool = False):
"""
Generate docker image, request the blender version (major, minor) with the keyword b3d_version, and can be
tagged with latest or only blender version info.
"""
client = docker.from_env()
if os.environ.get('docker_hub_password'):
client.login(username=os.environ['docker_hub_user'], password=os.environ['docker_hub_password'])
# Init all data to build the docker image
args = {
'b3d_vs_major': str(b3d_version[0]),
'b3d_vs_minor': str(b3d_version[1]),
}
docker_repo = 'stilobique/blender'
if latest:
tag = 'latest'
else:
tag = f'{args["b3d_vs_major"]}.{args["b3d_vs_minor"]}'
completed_tag_name = f'{docker_repo}:{args["b3d_vs_major"]}.{args["b3d_vs_minor"]}'
print('\t1 -> Build from image file')
client.images.build(path=os.getcwd(), buildargs=args, tag=completed_tag_name, rm=True, forcerm=True)
print(f'\t2 -> Push the new images with tag {tag}')
push = client.images.push(repository=docker_repo, tag=tag, stream=True)
print(f'\t\tDebug push :\n\t\t{push}')
if __name__ == "__main__": if __name__ == "__main__":
version = BlenderVersion(input("Give the blender version (X.Y.Z): ")) version = BlenderVersion(input("Give the blender version (X.Y.Z): "))
# build_docker(b3d_version=(f'{version.major}', f'{version.minor}.{version.hotfix}'),
# latest=False)
print(f"Build a blender version {version}.") print(f"Build a blender version {version}.")
docker = BlenderDocker(blender=version, severity=INFO) docker = BlenderDocker(version)
docker.build() docker.build()
print('Test your image.')
test = docker.test()
if test:
print('Test work, push this image.')
docker.push()
# Todo: Add a latest tag, check if this version are the latest
# Todo: Add a LTS tag, check if this version are a LTS
-2
View File
@@ -1,3 +1 @@
docker==7.2.0 docker==7.2.0
requests==2.34.2
invoke==3.0.3
+31
View File
@@ -0,0 +1,31 @@
#!/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
@@ -1,34 +0,0 @@
"""
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()