diff --git a/.run/Automation Builder.run.xml b/.run/Automation Builder.run.xml
index e9c8ff4..a24d670 100644
--- a/.run/Automation Builder.run.xml
+++ b/.run/Automation Builder.run.xml
@@ -8,8 +8,9 @@
+
-
+
diff --git a/.run/Clean docker Images .run.xml b/.run/Clean docker Images .run.xml
index fb9d001..e28429d 100644
--- a/.run/Clean docker Images .run.xml
+++ b/.run/Clean docker Images .run.xml
@@ -8,8 +8,9 @@
+
-
+
diff --git a/Dockerfile b/Dockerfile
index 3bb10c8..e52420e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,11 @@
-FROM ubuntu:25.10 AS b3dock
+# 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=5.2
@@ -8,32 +15,81 @@ 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 git git-lfs python3 sudo
+RUN apt-get update && apt-get install -y git git-lfs sudo
-# Compile Blender
+# 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 && \
- chmod a+x /opt/blender/build_files/build_environment/install_linux_packages.py && \
- python3 /opt/blender/build_files/build_environment/install_linux_packages.py
+ https://projects.blender.org/blender/blender.git /opt/blender
-RUN cd /opt/blender && \
- make update && \
- make -j$(nproc)
+# 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)
+
+# 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
## 02. Build optimissed image
## Setup a Multistage optimisation
FROM ubuntu:25.10 AS final
-#COPY --from=b3dock /opt/blender/build_files/build_environment/packages.txt /tmp/packages.txt
-COPY --from=b3dock /opt/blender/build_files/build_environment/install_linux_packages.py /tmp/install_linux_packages.py
-RUN apt-get update && apt-get install -y python3 sudo
-RUN python3 /tmp/install_linux_packages.py
+# 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
diff --git a/README.md b/README.md
index 97e8186..ec20fe9 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,22 @@
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`
@@ -59,9 +74,3 @@ python -m blender.cli get-tags-release
python -m blender.cli get-tags-docker
```
-
-# 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.
\ No newline at end of file
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
index 869b239..79bcd2c 100644
--- a/blender/docker.py
+++ b/blender/docker.py
@@ -3,6 +3,7 @@ import re
import docker
import logging
import datetime
+import requests
from .version import BlenderVersion
from .dependency import BlenderDependency
@@ -35,15 +36,31 @@ class BlenderDocker:
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__()
+ '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)
@@ -206,9 +223,42 @@ class BlenderDocker:
pass
def push(self):
- self._docker_client.images.push(
- repository=self._repository,
- tag=self._blender_version.tag_name(),
- stream=True,
- )
-
\ No newline at end of file
+ """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/main.py b/main.py
index 0c7d8ff..b3f3a4e 100644
--- a/main.py
+++ b/main.py
@@ -17,8 +17,8 @@ if __name__ == "__main__":
test = docker.test()
if test:
- print('Push this image.')
- # docker.push()
+ 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