diff --git a/.gitignore b/.gitignore
index 07ccd38..b9c2a64 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,9 @@
+### Logs
+**/logs/**
+
+### Config/setup """
+*.env
+
### PyCharm ###
.idea/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
deleted file mode 100644
index 908e3dc..0000000
--- a/.gitlab-ci.yml
+++ /dev/null
@@ -1,38 +0,0 @@
-cache:
- key: blender-source
- paths:
- - /opt/blender-git/blender # Blender git repository source
- - /opt/blender-git/lib # Blender Subversion lib package
- policy: pull
-
-update-cache:
- stage: .pre
- cache:
- key: blender-source
- image: stilobique/csv:latest
- script:
- - mkdir -p /opt/blender-git/lib
- - cd /opt/blender-git
- - git clone https://projects.blender.org/blender/blender.git
- - cd /opt/blender-git/blender
- - git submodule update --init --recursive
- - cd /opt/blender-git/lib
- - svn checkout https://svn.blender.org/svnroot/bf-blender/trunk/lib/linux_centos7_x86_64
-
-build-dockerfile:
- image: docker:19.03.12
- rules:
- - changes:
- - build.json
- stage: build
- services:
- - docker:dind
- cache:
- key: blender-source
- script:
- - apk add python3
- - apk add py3-pip
- - python3 -m pip install --upgrade pip
- - python3 -m pip install -r requirement-build.txt
- - python3 main.py
- - python3 main.py --tag=latest
diff --git a/.idea/icon.svg b/.idea/icon.svg
new file mode 100644
index 0000000..0b9bfa7
--- /dev/null
+++ b/.idea/icon.svg
@@ -0,0 +1,66 @@
+
+
diff --git a/.run/Generate image.run.xml b/.run/Automation Builder.run.xml
similarity index 52%
rename from .run/Generate image.run.xml
rename to .run/Automation Builder.run.xml
index 85e76e6..a24d670 100644
--- a/.run/Generate image.run.xml
+++ b/.run/Automation Builder.run.xml
@@ -1,27 +1,20 @@
-
-
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/.run/Dockerfile.run.xml b/.run/Build BlenderDocker.run.xml
similarity index 50%
rename from .run/Dockerfile.run.xml
rename to .run/Build BlenderDocker.run.xml
index a98f925..f4c50d0 100644
--- a/.run/Dockerfile.run.xml
+++ b/.run/Build BlenderDocker.run.xml
@@ -1,33 +1,26 @@
-
+
-
+
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/.run/Clean docker Images .run.xml b/.run/Clean docker Images .run.xml
new file mode 100644
index 0000000..e28429d
--- /dev/null
+++ b/.run/Clean docker Images .run.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index c58c1ea..e52420e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,59 +1,103 @@
-FROM ubuntu:22.04 AS blender
+# Pinned to the exact Python version Blender's own CMake requires for this
+# 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
-ARG b3d_vs_major=2.90
+ARG b3d_vs_major=5.2
ARG b3d_vs_minor=0
-ARG subversion=63014
LABEL Author="stilobique"
LABEL Title="Blender Docker for Unit Test"
+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
# Install dependencies
-RUN apt-get update && apt-get install -y \
- build-essential \
- gcc \
- git \
- cmake \
- libc6-dev \
- libx11-dev \
- subversion \
- libxrandr-dev \
- libxcursor-dev \
- libxxf86vm-dev \
- libxinerama-dev \
- libglew-dev \
- libxi-dev \
- python3
+RUN apt-get update && apt-get install -y git git-lfs sudo
-# Debug
-RUN echo Build Blender v${b3d_vs_major}.${b3d_vs_minor}
+# Get source code
+RUN git clone --depth 1 --branch v${b3d_vs_major}.${b3d_vs_minor} \
+ https://projects.blender.org/blender/blender.git /opt/blender
-ADD . /opt/blender-git
+# Start build
+# install_linux_packages.py (default, no --all) skips a handful of packages
+# 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 && \
+ chmod a+x ./install_linux_packages.py && \
+ python install_linux_packages.py && \
+ apt-get install -y libepoxy-dev libboost-all-dev libopenimageio-dev libembree-dev libpugixml-dev && \
+ pip install numpy cython && \
+ cd /opt/blender && make update && make -j$(nproc)
-# Compile Blender
-RUN cd /opt/blender-git/ \
- && git checkout v${b3d_vs_major}.${b3d_vs_minor} \
- && git config --global user.email "contact@aurelien-vaillant.net" \
- && git config --global user.name "Aurelien Vaillant" \
- && mkdir /opt/blender-git/lib \
- && cd /opt/blender-git/lib \
- && svn checkout -r${subversion} \
- && make
+# Discover Blender's *actual* runtime shared-library needs from the compiled
+# binary itself (ldd), instead of guessing from install_linux_packages.py
+# (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
-# Setup a Multistage optimisation
-FROM ubuntu:22.04
-COPY --from=blender /opt/blender-git/build_linux/bin /opt/blender
-ARG b3d_vs_major
-ENV B3D_ADDON_PATH "$HOME/.config/blender/${b3d_vs_major}/scripts/addons"
-RUN apt-get update && apt-get install -y \
- libxi6 \
- libxxf86vm1 \
- libxrender1 \
- libgl1-mesa-glx
+## 02. Build optimissed image
+## Setup a Multistage optimisation
+FROM ubuntu:25.10 AS final
+
+# Re-declared: ARGs before the first FROM only apply to that FROM line, not
+# to instructions in later stages.
+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.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
+
+USER bld
+COPY --from=b3dock /opt/build_linux/bin /opt/blender
+
+# ARG b3d_vs_major
+# ENV B3D_ADDON_PATH="/home/bld/.config/blender/${b3d_vs_major}/scripts/addons"
# Working Directory setup
-WORKDIR /
\ No newline at end of file
+WORKDIR /opt/blender
diff --git a/README.md b/README.md
index a913d12..ec20fe9 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,26 @@
-[](https://www.blender.org/download/)
-[](https://gitlab.com/stilobique/blender-docker/-/commits/main)
+[](https://www.blender.org/download/)
# Blender Docker
-This repository is an automated blender docker image file generated, it's usefully with blender addon unit test. To
-build a new specific image, you need to update the `build.json` file with the new version witched ; or
+This repository is an automated blender docker image file generated, it's usefully with blender addon unit test.
-# Adding an Addon
+# Building
+## Start your build
+You can use Invoke to easily start a build, a clean or more.
+```shell
+invoke --li
+```
+## 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.
Python function to install an addon with Blender. The file his named `install.py`
@@ -25,4 +38,39 @@ Call this function with blender inside this container
````shell
#!/bin/sh
/opt/blender/blender --background --python "install.py"
-````
\ No newline at end of file
+````
+
+# Check Image build
+```shell
+docker run --rm stilobique/blender:5.1.2 ./blender --version
+```
+
+```shell
+# Taille de l'image
+docker image inspect stilobique/blender:5.1.2 --format='{{.Size}}' | numfmt --to=iec
+
+# Dépendances manquantes
+docker run --rm --entrypoint /bin/bash stilobique/blender:5.1.2 -c 'ldd /opt/blender/blender | grep "not found" || echo "All deps OK"'
+
+# Test fonctionnel
+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
+```
+
diff --git a/blender/__init__.py b/blender/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/blender/dependency.py b/blender/dependency.py
new file mode 100644
index 0000000..c272cd4
--- /dev/null
+++ b/blender/dependency.py
@@ -0,0 +1,78 @@
+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/blender/blender/raw/tag/v{self._blender_version.tag_name()}/build_files/build_environment/install_linux_packages.py'
+ 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)
+
+ 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 + 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."""
+ 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__())
\ No newline at end of file
diff --git a/blender/docker.py b/blender/docker.py
new file mode 100644
index 0000000..79bcd2c
--- /dev/null
+++ b/blender/docker.py
@@ -0,0 +1,264 @@
+import os
+import re
+import docker
+import logging
+import datetime
+import requests
+
+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
+ # Todo get a user config
+ self._docker_client = docker.DockerClient(base_url=f'unix:///var/run/docker.sock')
+ self.login()
+ 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')
+ 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__(),
+ '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/blender/blender/raw/tag/'
+ f'v{self._blender_version.tag_name()}/build_files/cmake/Modules/FindPythonLibsUnix.cmake')
+ r = requests.get(url)
+ 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')
+ 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):
+ """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:
+ 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}')
+
+ # 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,
+ rm=True,
+ forcerm=True,
+ decode=True,
+ )
+
+ for chunk in build_logs:
+ collected_logs.append(chunk)
+
+ 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:
+ logger.exception(f'Build docker image failed: {e}')
+ raise
+
+ @staticmethod
+ def tag(self):
+ print('\tSet Tags version and/or Latest')
+ pass
+
+ def test(self) -> bool:
+ """
+ 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']
+ 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
+
+ def push(self):
+ """Push the Blender Docker Image"""
+ time = datetime.datetime.now()
+ 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
\ No newline at end of file
diff --git a/blender/misc.py b/blender/misc.py
new file mode 100644
index 0000000..abc401e
--- /dev/null
+++ b/blender/misc.py
@@ -0,0 +1,92 @@
+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
+ 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)
\ No newline at end of file
diff --git a/blender/version.py b/blender/version.py
new file mode 100644
index 0000000..4d8f076
--- /dev/null
+++ b/blender/version.py
@@ -0,0 +1,43 @@
+import re
+
+
+class BlenderVersion:
+ def __init__(self, version: str = '5.1.0'):
+ """
+ Need an object like 'X.Y.Z' to work correctly, build a pythonic object about a blender version.
+ :param version:
+ """
+ self._check_pattern(version)
+ self.major : str = ''
+ self.minor : str = ''
+ self.hotfix : str = ''
+ self.version_object(version)
+
+ def version_object(self, version: str):
+ """Set all version properties to a string"""
+ version = version.split('.')
+ self.major = version[0]
+ self.minor = version[1]
+ self.hotfix = version[2]
+
+ def format_to_tag(self, repository: str) -> str:
+ """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}'
+
+ 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):
+ return str(self.major + '.' + self.minor + '.' + self.hotfix)
\ No newline at end of file
diff --git a/build.json b/build.json
deleted file mode 100644
index 0092fee..0000000
--- a/build.json
+++ /dev/null
@@ -1,40 +0,0 @@
-{
- "tags":
- [{
- "b3d_vs_major": 3.4,
- "b3d_vs_minor": 1
- },
- {
- "b3d_vs_major": 3.4,
- "b3d_vs_minor": 0
- },
- {
- "b3d_vs_major": 3.3,
- "b3d_vs_minor": 2
- },
- {
- "b3d_vs_major": 3.3,
- "b3d_vs_minor": 1
- },
- {
- "b3d_vs_major": 3.3,
- "b3d_vs_minor": 0
- },
- {
- "b3d_vs_major": 3.2,
- "b3d_vs_minor": 1
- },
- {
- "b3d_vs_major": 3.2,
- "b3d_vs_minor": 0
- },
- {
- "b3d_vs_major": 3.1,
- "b3d_vs_minor": 2
- },
- {
- "b3d_vs_major": 3.1,
- "b3d_vs_minor": 0
- }
- ]
-}
\ No newline at end of file
diff --git a/image/Dockerfile-csv b/image/Dockerfile-csv
deleted file mode 100644
index ba97387..0000000
--- a/image/Dockerfile-csv
+++ /dev/null
@@ -1,7 +0,0 @@
-FROM alpine AS csv
-
-LABEL Author="stilobique"
-LABEL Title="Linux image with subversion and git"
-
-# Install dependencies
-RUN apk add git && apk add subversion
\ No newline at end of file
diff --git a/main.py b/main.py
index e1cf1c1..b3f3a4e 100644
--- a/main.py
+++ b/main.py
@@ -1,65 +1,24 @@
-import os
-import pathlib
-import json
-import docker
-import sys
+from logging import INFO
-# TODO Update readme file badge with blender version
-# TODO add a check if the image build are stored in the docker register
+from blender.version import BlenderVersion
+from blender.docker import BlenderDocker
-
-def get_data_from_json() -> (str, str):
- """From the json file, get blender version (major and minor), and return this information."""
- file = pathlib.Path(os.getcwd(), 'build.json')
-
- with open(file, 'r') as f:
- data = json.load(f)
- f.close()
-
- version_values = data['tags'][0]
- major, minor = version_values['b3d_vs_major'], version_values['b3d_vs_minor']
-
- return major, minor
-
-
-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}')
+# Todo Update readme file badge with blender version
if __name__ == "__main__":
- version = get_data_from_json()
- print(f'Start -> Setup the package with blender version "{version[0]}.{version[1]}"')
- for arg in sys.argv:
- if '--tag=' in arg:
- print(f'Build with the tag "{arg}".')
- build_docker(b3d_version=version, latest=True)
- break
- else:
- build_docker(b3d_version=version)
- print(f'Start to build the docker file.')
- pass
+ version = BlenderVersion(input("Give the blender version (X.Y.Z): "))
+
+ print(f"Build a blender version {version}.")
+ docker = BlenderDocker(blender=version, severity=INFO)
+ 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
\ No newline at end of file
diff --git a/requirement-build.txt b/requirement-build.txt
deleted file mode 100644
index 6d0eac4..0000000
--- a/requirement-build.txt
+++ /dev/null
@@ -1 +0,0 @@
-docker
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..827604f
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+docker==7.2.0
+requests==2.34.2
+invoke==3.0.3
\ No newline at end of file
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