Build, test and push

This commit is contained in:
2026-07-24 23:50:12 +02:00
parent d57e2ac46c
commit 7734668166
7 changed files with 232 additions and 37 deletions
+2 -1
View File
@@ -8,8 +8,9 @@
<env name="PYTHONUNBUFFERED" value="1" /> <env name="PYTHONUNBUFFERED" value="1" />
</envs> </envs>
<option name="SDK_HOME" value="" /> <option name="SDK_HOME" value="" />
<option name="SDK_NAME" value="Python 3.12 (BlenderDocker)" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" /> <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" /> <option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" /> <option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" /> <option name="ADD_SOURCE_ROOTS" value="true" />
<option name="DEBUG_JUST_MY_CODE" value="false" /> <option name="DEBUG_JUST_MY_CODE" value="false" />
+2 -1
View File
@@ -8,8 +8,9 @@
<env name="PYTHONUNBUFFERED" value="1" /> <env name="PYTHONUNBUFFERED" value="1" />
</envs> </envs>
<option name="SDK_HOME" value="" /> <option name="SDK_HOME" value="" />
<option name="SDK_NAME" value="Python 3.12 (BlenderDocker)" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/" /> <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/" />
<option name="IS_MODULE_SDK" value="true" /> <option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" /> <option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" /> <option name="ADD_SOURCE_ROOTS" value="true" />
<option name="DEBUG_JUST_MY_CODE" value="false" /> <option name="DEBUG_JUST_MY_CODE" value="false" />
+72 -16
View File
@@ -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 # Setup all software version request
ARG b3d_vs_major=5.2 ARG b3d_vs_major=5.2
@@ -8,32 +15,81 @@ 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 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} \ 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
chmod a+x /opt/blender/build_files/build_environment/install_linux_packages.py && \
python3 /opt/blender/build_files/build_environment/install_linux_packages.py
RUN cd /opt/blender && \ # Start build
make update && \ # install_linux_packages.py (default, no --all) skips a handful of packages
make -j$(nproc) # 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 ## 02. Build optimissed image
## Setup a Multistage optimisation ## Setup a Multistage optimisation
FROM ubuntu:25.10 AS final FROM ubuntu:25.10 AS final
#COPY --from=b3dock /opt/blender/build_files/build_environment/packages.txt /tmp/packages.txt # Re-declared: ARGs before the first FROM only apply to that FROM line, not
COPY --from=b3dock /opt/blender/build_files/build_environment/install_linux_packages.py /tmp/install_linux_packages.py # to instructions in later stages.
RUN apt-get update && apt-get install -y python3 sudo ARG python_version=3.13
RUN python3 /tmp/install_linux_packages.py
# 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
+16 -7
View File
@@ -5,7 +5,22 @@
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.
# 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. 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`
@@ -59,9 +74,3 @@ python -m blender.cli get-tags-release
python -m blender.cli get-tags-docker 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.
+78
View File
@@ -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__())
+52 -2
View File
@@ -3,6 +3,7 @@ import re
import docker import docker
import logging import logging
import datetime import datetime
import requests
from .version import BlenderVersion from .version import BlenderVersion
from .dependency import BlenderDependency from .dependency import BlenderDependency
@@ -39,11 +40,27 @@ class BlenderDocker:
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__() 'b3d_dependency': self._packages.__str__(),
'python_version': self._get_required_python_version(),
} }
self._severity = severity 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: def _get_logger(self, name: str) -> logging.Logger:
""" """
Private Method to set a logger for a specific method set (by his name) Private Method to set a logger for a specific method set (by his name)
@@ -206,9 +223,42 @@ class BlenderDocker:
pass pass
def push(self): def push(self):
self._docker_client.images.push( """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, repository=self._repository,
tag=self._blender_version.tag_name(), tag=self._blender_version.tag_name(),
stream=True, 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
+2 -2
View File
@@ -17,8 +17,8 @@ if __name__ == "__main__":
test = docker.test() test = docker.test()
if test: if test:
print('Push this image.') print('Test work, push this image.')
# docker.push() docker.push()
# Todo: Add a latest tag, check if this version are the latest # Todo: Add a latest tag, check if this version are the latest
# Todo: Add a LTS tag, check if this version are a LTS # Todo: Add a LTS tag, check if this version are a LTS