Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/py310/tesseract_config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: "py310"
version: "0.1.0"
description: |
Empty Tesseract that requires Python 3.10 (set through a custom Docker image).
Empty Tesseract that requires Python 3.10 (set through build_config.python_version).

build_config:
base_image: "python:3.10-slim-bookworm"
python_version: "3.10"
21 changes: 21 additions & 0 deletions tesseract_core/sdk/api_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Field,
Strict,
field_validator,
model_validator,
)
from pydantic import ValidationError as PydanticValidationError

Expand Down Expand Up @@ -132,11 +133,31 @@ class TesseractBuildConfig(BaseModel, validate_assignment=True):
"Example: ``[\"RUN echo 'Hello, world!'\"]``"
),
)
python_version: StrictStr | None = Field(
None,
description=(
"Python version to use inside the Tesseract (e.g., '3.12'). "
"When set, ``uv python install`` is used to install the specified version, "
"decoupling the Python version from the base image. "
"When unset, the system Python from the base image is used."
),
)

requirements: PythonRequirements = PipRequirements(provider="python-pip")

model_config = ConfigDict(extra="forbid")

@model_validator(mode="after")
def _validate_python_version_provider(self):
if self.python_version is not None and isinstance(
self.requirements, CondaRequirements
):
raise ValueError(
"python_version cannot be used with conda requirements. "
"Set the Python version in tesseract_environment.yaml instead."
)
return self

skip_checks: bool = Field(
False,
description=(
Expand Down
6 changes: 6 additions & 0 deletions tesseract_core/sdk/templates/Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ COPY {{ tesseract_source_directory }}/{{ config.build_config.requirements._filen
COPY {{ config.build_config.requirements._build_script }} ./
COPY local_requirements/ ./local_requirements

{% if config.build_config.python_version %}
ENV TESSERACT_PYTHON_VERSION="{{ config.build_config.python_version }}"
{% endif %}

# Build a python venv from python provider build scripts.
# The build script has to create a venv at /python-env
RUN --mount=type=cache,target=/root/.cache/uv {% if use_ssh_mount %}--mount=type=ssh{% endif %} bash {{ config.build_config.requirements._build_script }}
Expand All @@ -69,7 +73,9 @@ USER root

RUN apt-get update && apt-get install -y --no-install-recommends \
libnss-wrapper \
{%- if not config.build_config.python_version %}
&& { [ -x "$(command -v python3)" ] || apt-get install -y --no-install-recommends python3; } \
{%- endif %}
&& rm -rf /var/lib/apt/lists/*

{% if config.build_config.extra_packages %}
Expand Down
6 changes: 6 additions & 0 deletions tesseract_core/sdk/templates/base/tesseract_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ build_config:
# Base image to use for the container, must be Ubuntu or Debian-based
# base_image: "debian:bookworm-slim"

# Python version to use inside the container (e.g. "3.12"). When set, the given
# version is installed via `uv python install`, decoupling it from the base
# image. When unset, the base image's system Python is used. Cannot be combined
# with conda requirements; set the version in tesseract_environment.yaml instead.
# python_version: "3.12"

# Platform to build the container for. In general, images can only be executed
# on the platform they were built for.
target_platform: "native"
Expand Down
24 changes: 23 additions & 1 deletion tesseract_core/sdk/templates/build_pip_venv.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@

set -e # Exit immediately if a command exits with a non-zero status

uv venv /python-env
if [ -n "${TESSERACT_PYTHON_VERSION:-}" ]; then
uv python install "$TESSERACT_PYTHON_VERSION"
uv venv --python "$TESSERACT_PYTHON_VERSION" /python-env
else
uv venv /python-env
fi
source /python-env/bin/activate

# Collect dependencies
Expand All @@ -28,3 +33,20 @@ uv -v pip install --compile-bytecode ./tesseract_runtime

# Install pip itself into the virtual environment for use by any custom build steps
uv pip install pip

if [ -n "${TESSERACT_PYTHON_VERSION:-}" ]; then
# The venv's python binary is a symlink into the uv-managed installation
# (e.g. /root/.local/share/uv/python/cpython-3.12-.../). Merge that
# installation (stdlib, binary) into /python-env so the venv is fully
# self-contained after Docker multi-stage COPY (which preserves symlinks).
UV_PYTHON_DIR=$(dirname "$(dirname "$(readlink -f /python-env/bin/python)")")
rm /python-env/bin/python /python-env/bin/python3 /python-env/bin/python3.*
cp -a "$UV_PYTHON_DIR"/bin/* /python-env/bin/
cp -a "$UV_PYTHON_DIR"/lib/* /python-env/lib/
cp -a "$UV_PYTHON_DIR"/include/* /python-env/include/

# pyvenv.cfg still points `home` at the uv-managed installation, which does
# not exist after the Docker multi-stage COPY. Point it at the now-local
# binaries so the interpreter can locate its stdlib.
sed -i "s|^home = .*|home = /python-env/bin|" /python-env/pyvenv.cfg
fi
6 changes: 6 additions & 0 deletions tesseract_core/sdk/templates/jax/tesseract_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ build_config:
# Base image to use for the container, must be Ubuntu or Debian-based
# base_image: "debian:bookworm-slim"

# Python version to use inside the container (e.g. "3.12"). When set, the given
# version is installed via `uv python install`, decoupling it from the base
# image. When unset, the base image's system Python is used. Cannot be combined
# with conda requirements; set the version in tesseract_environment.yaml instead.
# python_version: "3.12"

# Platform to build the container for. In general, images can only be executed
# on the platform they were built for.
target_platform: "native"
Expand Down
6 changes: 6 additions & 0 deletions tesseract_core/sdk/templates/pytorch/tesseract_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ build_config:
# Base image to use for the container, must be Ubuntu or Debian-based
# base_image: "debian:bookworm-slim"

# Python version to use inside the container (e.g. "3.12"). When set, the given
# version is installed via `uv python install`, decoupling it from the base
# image. When unset, the base image's system Python is used. Cannot be combined
# with conda requirements; set the version in tesseract_environment.yaml instead.
# python_version: "3.12"

# Platform to build the container for. In general, images can only be executed
# on the platform they were built for.
target_platform: "native"
Expand Down
44 changes: 44 additions & 0 deletions tests/sdk_tests/test_api_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,50 @@ def test_optional_signature_check(
validate_tesseract_api(tmp_path)


def test_config_with_python_version(
tmp_path, valid_tesseract_api, valid_tesseract_config
):
_write_tesseract_api_to_file(valid_tesseract_api, tmp_path)

config_with_python_version = yaml.safe_load(valid_tesseract_config)
config_with_python_version["build_config"]["python_version"] = "3.12"
_write_tesseract_config_to_file(yaml.dump(config_with_python_version), tmp_path)
validate_tesseract_api(tmp_path)

from tesseract_core.sdk.api_parse import get_config

config = get_config(tmp_path)
assert config.build_config.python_version == "3.12"


def test_config_python_version_rejects_conda(
tmp_path, valid_tesseract_api, valid_tesseract_config
):
_write_tesseract_api_to_file(valid_tesseract_api, tmp_path)

config = yaml.safe_load(valid_tesseract_config)
config["build_config"]["python_version"] = "3.12"
config["build_config"]["requirements"] = {"provider": "conda"}
_write_tesseract_config_to_file(yaml.dump(config), tmp_path)

with pytest.raises(
ValidationError, match="python_version cannot be used with conda"
):
validate_tesseract_api(tmp_path)


def test_config_python_version_defaults_to_none(
tmp_path, valid_tesseract_api, valid_tesseract_config
):
_write_tesseract_api_to_file(valid_tesseract_api, tmp_path)
_write_tesseract_config_to_file(valid_tesseract_config, tmp_path)

from tesseract_core.sdk.api_parse import get_config

config = get_config(tmp_path)
assert config.build_config.python_version is None


def test_schema_parent_class_is_checked(
tmp_path, valid_tesseract_api, valid_tesseract_config
):
Expand Down
24 changes: 24 additions & 0 deletions tests/sdk_tests/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,30 @@ def test_prepare_build_context(tmp_path_factory):
assert (build_dir / "Dockerfile").exists()


def test_prepare_build_context_python_version(tmp_path_factory):
"""Test that python_version is rendered as ENV in the Dockerfile."""
src_dir = tmp_path_factory.mktemp("src")
(src_dir / "foo").touch()
build_dir = tmp_path_factory.mktemp("build")

config = TesseractConfig(
name="foobar",
build_config=TesseractBuildConfig(python_version="3.12"),
)
engine.prepare_build_context(src_dir, build_dir, config)

dockerfile = (build_dir / "Dockerfile").read_text()
assert 'TESSERACT_PYTHON_VERSION="3.12"' in dockerfile

# Without python_version, the env var should not appear
build_dir2 = tmp_path_factory.mktemp("build2")
config_default = TesseractConfig(name="foobar")
engine.prepare_build_context(src_dir, build_dir2, config_default)

dockerfile_default = (build_dir2 / "Dockerfile").read_text()
assert "TESSERACT_PYTHON_VERSION" not in dockerfile_default


def test_prepare_build_context_env(tmp_path_factory):
"""Test that env variables are rendered as ENV lines in the Dockerfile."""
src_dir = tmp_path_factory.mktemp("src")
Expand Down
Loading