Compare commits
5
Commits
1457177f6b
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a561a35030 | ||
|
|
467a6ce0e3 | ||
|
|
77075e369c | ||
|
|
2b994e04cb | ||
|
|
c0afb5c9bc |
@@ -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" />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="Build BlenderDocker" type="docker-deploy" factoryName="dockerfile" server-name="Docker Desktop">
|
<configuration default="false" name="Build BlenderDocker" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||||
<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" />
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,33 @@
|
|||||||
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
|
||||||
|
## 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`
|
||||||
@@ -40,3 +66,22 @@ 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
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
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__())
|
||||||
+148
-35
@@ -1,17 +1,29 @@
|
|||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import docker
|
import docker
|
||||||
import logging
|
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, APIError, ContainerError, ImageNotFound
|
||||||
|
|
||||||
|
|
||||||
|
_ANSI_ESCAPE_RE = re.compile(r'\x1b\[[0-9;]*[a-zA-Z]')
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_log_line(text: str) -> str:
|
||||||
|
"""Strip ANSI escape codes and keep only the last update of a \r-rewritten line."""
|
||||||
|
text = _ANSI_ESCAPE_RE.sub('', text)
|
||||||
|
return text.split('\r')[-1].strip()
|
||||||
|
|
||||||
|
|
||||||
class BlenderDocker:
|
class BlenderDocker:
|
||||||
def __init__(self, blender: BlenderVersion, severity: int = logging.WARNING):
|
def __init__(self, blender: BlenderVersion, severity: int = logging.WARNING):
|
||||||
self._blender_version = blender
|
self._blender_version = blender
|
||||||
# Todo get a user config
|
self._docker_client = docker.from_env()
|
||||||
self._docker_client = docker.DockerClient(base_url=f'unix:///var/run/docker.sock')
|
|
||||||
self.login()
|
self.login()
|
||||||
self._docker_image = None
|
self._docker_image = None
|
||||||
self._repository = 'stilobique/blender'
|
self._repository = 'stilobique/blender'
|
||||||
@@ -22,13 +34,33 @@ class BlenderDocker:
|
|||||||
print(f'Dockerfile not found: {self._docker_file}')
|
print(f'Dockerfile not found: {self._docker_file}')
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
|
self._packages = BlenderDependency(blender)
|
||||||
|
|
||||||
self._args = {
|
self._args = {
|
||||||
'b3d_vs_major': f'{self._blender_version.major}',
|
'b3d_vs_major': f'{self._blender_version.major}',
|
||||||
'b3d_vs_minor': f'{self._blender_version.minor}.{self._blender_version.hotfix}',
|
'b3d_vs_minor': f'{self._blender_version.minor}.{self._blender_version.hotfix}',
|
||||||
|
'b3d_dependency': self._packages.__str__(),
|
||||||
|
'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/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:
|
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)
|
||||||
@@ -43,7 +75,7 @@ class BlenderDocker:
|
|||||||
logger.propagate = False
|
logger.propagate = False
|
||||||
|
|
||||||
if not logger.handlers:
|
if not logger.handlers:
|
||||||
handler = logging.FileHandler(log_dir / f'{name}.log')
|
handler = logging.FileHandler(log_dir / f'{name}.log', encoding='utf-8')
|
||||||
handler.setFormatter(logging.Formatter(
|
handler.setFormatter(logging.Formatter(
|
||||||
'%(asctime)s | [%(levelname)s] %(message)s'
|
'%(asctime)s | [%(levelname)s] %(message)s'
|
||||||
))
|
))
|
||||||
@@ -66,39 +98,56 @@ class BlenderDocker:
|
|||||||
|
|
||||||
def build(self):
|
def build(self):
|
||||||
"""Build the Blender Docker Image"""
|
"""Build the Blender Docker Image"""
|
||||||
logger = self._get_logger('build')
|
time = datetime.datetime.now()
|
||||||
|
logger = self._get_logger(f'build-{time.strftime("%Y-%m-%d-%H-%M")}')
|
||||||
|
collected_logs = []
|
||||||
try:
|
try:
|
||||||
print('Building Blender Docker Image')
|
print('Building Blender Docker Image')
|
||||||
logger.info('Building Blender Docker Image')
|
logger.info('Building Blender Docker Image')
|
||||||
logger.info(f'\tBuilds withs args: {self._args}')
|
logger.info(f'\tBuilds withs args: {self._args}')
|
||||||
logger.info(f'\tTag generate: {self._docker_image_name}')
|
logger.info(f'\tTag generate: {self._docker_image_name}')
|
||||||
|
|
||||||
self._docker_image, build_logs = self._docker_client.images.build(
|
# Use the low-level API: images.build() only returns once the whole
|
||||||
|
# build is finished, so it gives no live progress. api.build() with
|
||||||
|
# decode=True streams each step (including cache hits) as it happens.
|
||||||
|
build_logs = self._docker_client.api.build(
|
||||||
path=os.getcwd(),
|
path=os.getcwd(),
|
||||||
# nocache=True,
|
# nocache=True,
|
||||||
buildargs=self._args,
|
buildargs=self._args,
|
||||||
tag=self._docker_image_name,
|
tag=self._docker_image_name,
|
||||||
quiet=False,
|
|
||||||
rm=True,
|
rm=True,
|
||||||
forcerm=True
|
forcerm=True,
|
||||||
|
decode=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Debug log:
|
|
||||||
for chunk in build_logs:
|
for chunk in build_logs:
|
||||||
|
collected_logs.append(chunk)
|
||||||
|
|
||||||
if 'stream' in chunk:
|
if 'stream' in chunk:
|
||||||
line = chunk['stream'].strip()
|
for raw_line in chunk['stream'].split('\n'):
|
||||||
if line:
|
line = _clean_log_line(raw_line)
|
||||||
logger.debug(line)
|
if not line:
|
||||||
|
continue
|
||||||
|
if 'Using cache' in line:
|
||||||
|
line = f'[CACHE] {line}'
|
||||||
|
logger.info(line)
|
||||||
|
|
||||||
|
elif 'status' in chunk:
|
||||||
|
status = _clean_log_line(chunk['status'])
|
||||||
|
progress = _clean_log_line(chunk.get('progress', ''))
|
||||||
|
line = f"{status} {chunk.get('id', '')} {progress}".strip()
|
||||||
|
if 'Already exists' in status:
|
||||||
|
line = f'[CACHE] {line}'
|
||||||
|
logger.info(line)
|
||||||
|
|
||||||
elif 'error' in chunk:
|
elif 'error' in chunk:
|
||||||
logger.error(chunk['error'])
|
logger.error(chunk['error'])
|
||||||
|
raise BuildError(chunk['error'], collected_logs)
|
||||||
|
|
||||||
|
self._docker_image = self._docker_client.images.get(self._docker_image_name)
|
||||||
|
|
||||||
except BuildError as e:
|
except BuildError as e:
|
||||||
logger.exception(f'Build docker image failed: {e}')
|
logger.exception(f'Build docker image failed: {e}')
|
||||||
for line in e.build_log:
|
|
||||||
if 'stream' in line:
|
|
||||||
logger.debug(line['stream'].strip())
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -106,46 +155,110 @@ class BlenderDocker:
|
|||||||
print('\tSet Tags version and/or Latest')
|
print('\tSet Tags version and/or Latest')
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test(self):
|
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."""
|
"""
|
||||||
|
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')
|
logger = self._get_logger('test')
|
||||||
|
container_name = f"Blender-Docker-Testing-{self._blender_version.tag_name_slugify()}"
|
||||||
|
command = ['./blender', '--version']
|
||||||
|
container = None
|
||||||
try:
|
try:
|
||||||
print('Testing your Blender Docker image')
|
print(f'Testing your Blender Docker image {self._docker_image_name}')
|
||||||
logger.info('Testing your Blender Docker image')
|
logger.info(f'Testing image "{self._docker_image_name}" in container "{container_name}" '
|
||||||
|
f'with command: {" ".join(command)}')
|
||||||
|
|
||||||
test = self._docker_client.containers.run(
|
# auto_remove is intentionally NOT used: Docker can remove the container
|
||||||
name=f"Blender-Docker-Testing-{self._blender_version.tag_name_slugify()}",
|
# 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,
|
image=self._docker_image_name,
|
||||||
command=['./blender', '--version'],
|
command=command,
|
||||||
auto_remove = True,
|
|
||||||
remove=True,
|
|
||||||
detach=True,
|
detach=True,
|
||||||
stdout=True
|
stdout=True,
|
||||||
|
stderr=True,
|
||||||
)
|
)
|
||||||
|
result = container.wait()
|
||||||
|
exit_code = result.get('StatusCode')
|
||||||
|
|
||||||
print(f'\tResult: {test.logs()}')
|
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}')
|
||||||
|
|
||||||
return True
|
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:
|
except ContainerError as e:
|
||||||
|
logger.exception(f'Container error while testing "{self._docker_image_name}": {e}')
|
||||||
print(f'Container error: {e}')
|
print(f'Container error: {e}')
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
except ImageNotFound as e:
|
except ImageNotFound as e:
|
||||||
|
logger.exception(f'Image "{self._docker_image_name}" not found: {e}')
|
||||||
print(f'Image not found: {e}')
|
print(f'Image not found: {e}')
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
except APIError as e:
|
except APIError as e:
|
||||||
|
logger.exception(f'API error while testing "{self._docker_image_name}": {e}')
|
||||||
print(f'API error: {e}')
|
print(f'API error: {e}')
|
||||||
for line in e.build_log:
|
|
||||||
if 'stream' in line:
|
|
||||||
print(line['stream'].strip())
|
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
def push(self):
|
finally:
|
||||||
self._docker_client.images.push(
|
if container is not None:
|
||||||
repository=self._repository,
|
try:
|
||||||
tag=self._blender_version.tag_name(),
|
container.remove(force=True)
|
||||||
stream=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
|
||||||
+25
-6
@@ -1,16 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
import requests
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from subprocess import run, CalledProcessError
|
from subprocess import run, CalledProcessError
|
||||||
|
|
||||||
# def clean_all_dockers():
|
|
||||||
# # bash docker context use default && docker system prune -a -f
|
|
||||||
# # bash docker context use podman && docker system prune -a -f
|
|
||||||
# # bash docker context use desktop-linux && docker system prune -a -f
|
|
||||||
# docker_client = docker.DockerClient(base_url=f'unix:///var/run/docker.sock')
|
|
||||||
|
|
||||||
|
|
||||||
def clean_all_dockers():
|
def clean_all_dockers():
|
||||||
"""
|
"""
|
||||||
@@ -75,5 +70,29 @@ def clean_all_dockers():
|
|||||||
logger.info(f'Contexte 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__':
|
if __name__ == '__main__':
|
||||||
|
print('Clean docker')
|
||||||
clean_all_dockers()
|
clean_all_dockers()
|
||||||
|
print('Done')
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
class BlenderVersion:
|
class BlenderVersion:
|
||||||
@@ -6,6 +7,7 @@ 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 = ''
|
||||||
@@ -29,5 +31,13 @@ class BlenderVersion:
|
|||||||
def tag_name_slugify(self):
|
def tag_name_slugify(self):
|
||||||
return f'{self.major}-{self.minor}-{self.hotfix}'
|
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)
|
||||||
@@ -7,7 +7,6 @@ from blender.docker import BlenderDocker
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Todo use the manual entry can be error prone (need to make a basic regex to check this entry)
|
|
||||||
version = BlenderVersion(input("Give the blender version (X.Y.Z): "))
|
version = BlenderVersion(input("Give the blender version (X.Y.Z): "))
|
||||||
|
|
||||||
print(f"Build a blender version {version}.")
|
print(f"Build a blender version {version}.")
|
||||||
@@ -18,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
|
||||||
@@ -1 +1,3 @@
|
|||||||
docker==7.2.0
|
docker==7.2.0
|
||||||
|
requests==2.34.2
|
||||||
|
invoke==3.0.3
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Lit install_linux_packages.py et génère un fichier .txt avec les paquets
|
|
||||||
Debian/Ubuntu à installer.
|
|
||||||
"""
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
READER="install_linux_packages.py"
|
|
||||||
OUT="packages.txt"
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
with open(READER, "r", encoding="utf-8") as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
# Cherche toutes les lignes du type: DISTRO_ID_DEBIAN: "nom-du-paquet"
|
|
||||||
packages = re.findall(r'DISTRO_ID_DEBIAN:\s*"([^"]+)"', content)
|
|
||||||
|
|
||||||
# Enlève les doublons, trie
|
|
||||||
packages = sorted(set(packages))
|
|
||||||
|
|
||||||
with open(OUT, "w", encoding="utf-8") as f:
|
|
||||||
for pkg in packages:
|
|
||||||
f.write(pkg + "\n")
|
|
||||||
|
|
||||||
print(f"{len(packages)} paquets écrits dans {OUT}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""
|
||||||
|
Invoke CLI entry point for the reusable helpers in `blender/misc.py`.
|
||||||
|
Ex: python -m blender.cli clean
|
||||||
|
Ex: python -m blender.cli get-tags-release
|
||||||
|
"""
|
||||||
|
|
||||||
|
from invoke import Collection, Program, task
|
||||||
|
|
||||||
|
from blender.misc import clean_all_dockers, get_all_tags_from_blender, get_all_tags_from_docker
|
||||||
|
|
||||||
|
|
||||||
|
@task(name='clean')
|
||||||
|
def clean(ctx):
|
||||||
|
"""Nettoie Docker pour tous les contextes disponibles (default, podman, ...)."""
|
||||||
|
clean_all_dockers()
|
||||||
|
|
||||||
|
|
||||||
|
@task(name='get-tags-release')
|
||||||
|
def get_tags_release(ctx):
|
||||||
|
"""Recupere tous les tags de release Blender depuis Gitea."""
|
||||||
|
get_all_tags_from_blender()
|
||||||
|
|
||||||
|
|
||||||
|
@task(name='get-tags-docker')
|
||||||
|
def get_tags_docker(ctx):
|
||||||
|
"""Recupere tous les tags de l'image Docker stilobique/blender depuis Docker Hub."""
|
||||||
|
get_all_tags_from_docker()
|
||||||
|
|
||||||
|
|
||||||
|
namespace = Collection(clean, get_tags_release, get_tags_docker)
|
||||||
|
program = Program(namespace=namespace, version='0.1.0')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
program.run()
|
||||||
Reference in New Issue
Block a user