Move runtime.txt handling into base class

This will allow it to be used for Python (and other languages) in future
pull/1428/head
Simon Li 2025-06-22 14:22:51 +01:00
rodzic e795060aec
commit cab6fce923
5 zmienionych plików z 94 dodań i 42 usunięć

Wyświetl plik

@ -1,3 +1,4 @@
import datetime
import hashlib import hashlib
import io import io
import logging import logging
@ -750,3 +751,41 @@ class BaseImage(BuildPack):
# the only path evaluated at container start time rather than build time # the only path evaluated at container start time rather than build time
return os.path.join("${REPO_DIR}", start) return os.path.join("${REPO_DIR}", start)
return None return None
@property
def runtime_info(self):
"""
Return parsed contents of runtime.txt
Returns (runtime, version, date), tuple components may be None.
Returns (None, None, None) if runtime.txt not found.
"""
if hasattr(self, "_runtime"):
return self._runtime
self._runtime = (None, None, None)
runtime_path = self.binder_path("runtime.txt")
try:
with open(runtime_path) as f:
runtime_txt = f.read().strip()
except FileNotFoundError:
return self._runtime
runtime = None
version = None
date = None
parts = runtime_txt.split("-", 2)
if len(parts) > 0 and parts[0]:
runtime = parts[0]
if len(parts) > 1 and parts[1]:
version = parts[1]
if len(parts) > 2 and parts[2]:
date = parts[2]
if not re.match(r"\d\d\d\d-\d\d-\d\d", date):
raise ValueError(f"Invalid date, expected YYYY-MM-DD: {date}")
date = datetime.datetime.fromisoformat(date).date()
self._runtime = (runtime, version, date)
return self._runtime

Wyświetl plik

@ -187,12 +187,9 @@ class PipfileBuildPack(CondaBuildPack):
def detect(self): def detect(self):
"""Check if current repo should be built with the Pipfile buildpack.""" """Check if current repo should be built with the Pipfile buildpack."""
# first make sure python is not explicitly unwanted # first make sure python is not explicitly unwanted
runtime_txt = self.binder_path("runtime.txt") runtime = self.runtime_info[0]
if os.path.exists(runtime_txt): if runtime and runtime != "python":
with open(runtime_txt) as f: return False
runtime = f.read().strip()
if not runtime.startswith("python-"):
return False
pipfile = self.binder_path("Pipfile") pipfile = self.binder_path("Pipfile")
pipfile_lock = self.binder_path("Pipfile.lock") pipfile_lock = self.binder_path("Pipfile.lock")

Wyświetl plik

@ -15,14 +15,10 @@ class PythonBuildPack(CondaBuildPack):
if hasattr(self, "_python_version"): if hasattr(self, "_python_version"):
return self._python_version return self._python_version
try: runtime, version, _ = self.runtime_info
with open(self.binder_path("runtime.txt")) as f:
runtime = f.read().strip()
except FileNotFoundError:
runtime = ""
if not runtime.startswith("python-"): if runtime != "python" or not version:
# not a Python runtime (e.g. R, which subclasses this) # Either not specified, or not a Python runtime (e.g. R, which subclasses this)
# use the default Python # use the default Python
self._python_version = self.major_pythons["3"] self._python_version = self.major_pythons["3"]
self.log.warning( self.log.warning(
@ -30,7 +26,7 @@ class PythonBuildPack(CondaBuildPack):
) )
return self._python_version return self._python_version
py_version_info = runtime.split("-", 1)[1].split(".") py_version_info = version.split(".")
py_version = "" py_version = ""
if len(py_version_info) == 1: if len(py_version_info) == 1:
py_version = self.major_pythons[py_version_info[0]] py_version = self.major_pythons[py_version_info[0]]
@ -138,16 +134,10 @@ class PythonBuildPack(CondaBuildPack):
def detect(self): def detect(self):
"""Check if current repo should be built with the Python buildpack.""" """Check if current repo should be built with the Python buildpack."""
requirements_txt = self.binder_path("requirements.txt") requirements_txt = self.binder_path("requirements.txt")
runtime_txt = self.binder_path("runtime.txt")
setup_py = "setup.py" setup_py = "setup.py"
if os.path.exists(runtime_txt): if self.runtime_info[0]:
with open(runtime_txt) as f: return self.runtime_info[0] == "python"
runtime = f.read().strip()
if runtime.startswith("python-"):
return True
else:
return False
if not self.binder_dir and os.path.exists(setup_py): if not self.binder_dir and os.path.exists(setup_py):
return True return True
return os.path.exists(requirements_txt) return os.path.exists(requirements_txt)

Wyświetl plik

@ -1,6 +1,6 @@
import datetime import datetime
import os import os
import re import warnings
from functools import lru_cache from functools import lru_cache
import requests import requests
@ -49,7 +49,14 @@ class RBuildPack(PythonBuildPack):
def runtime(self): def runtime(self):
""" """
Return contents of runtime.txt if it exists, '' otherwise Return contents of runtime.txt if it exists, '' otherwise
Deprecated, use `runtime_info` instead.
""" """
warnings.warn(
"`{self.__class__.__name__}.runtime` is deprecated. Use `runtime_info` instead",
DeprecationWarning,
)
if not hasattr(self, "_runtime"): if not hasattr(self, "_runtime"):
runtime_path = self.binder_path("runtime.txt") runtime_path = self.binder_path("runtime.txt")
try: try:
@ -90,11 +97,11 @@ class RBuildPack(PythonBuildPack):
r_version = version_map["4.2"] r_version = version_map["4.2"]
if not hasattr(self, "_r_version"): if not hasattr(self, "_r_version"):
parts = self.runtime.split("-") runtime, version, date = self.runtime_info
# If runtime.txt is not set, or if it isn't of the form r-<version>-<yyyy>-<mm>-<dd>, # If runtime.txt is not set, or if it isn't of the form r-<version>-<yyyy>-<mm>-<dd>,
# we don't use any of it in determining r version and just use the default # we don't use any of it in determining r version and just use the default
if len(parts) == 5: if version and date:
r_version = parts[1] r_version = version
# For versions of form x.y, we want to explicitly provide x.y.z - latest patchlevel # For versions of form x.y, we want to explicitly provide x.y.z - latest patchlevel
# available. Users can however explicitly specify the full version to get something specific # available. Users can however explicitly specify the full version to get something specific
if r_version in version_map: if r_version in version_map:
@ -116,15 +123,11 @@ class RBuildPack(PythonBuildPack):
Returns '' if no date is specified Returns '' if no date is specified
""" """
if not hasattr(self, "_checkpoint_date"): if not hasattr(self, "_checkpoint_date"):
match = re.match(r"r-(\d.\d(.\d)?-)?(\d\d\d\d)-(\d\d)-(\d\d)", self.runtime) runtime, version, date = self.runtime_info
if not match: if runtime == "r" and date:
self._checkpoint_date = False self._checkpoint_date = date
else: else:
# turn the last three groups of the match into a date self._checkpoint_date = False
self._checkpoint_date = datetime.date(
*[int(s) for s in match.groups()[-3:]]
)
return self._checkpoint_date return self._checkpoint_date
def detect(self): def detect(self):
@ -142,13 +145,9 @@ class RBuildPack(PythonBuildPack):
description_R = "DESCRIPTION" description_R = "DESCRIPTION"
if not self.binder_dir and os.path.exists(description_R): if not self.binder_dir and os.path.exists(description_R):
if not self.checkpoint_date: # no R snapshot date set through runtime.txt
# no R snapshot date set through runtime.txt # Set it to two days ago from today
# Set it to two days ago from today self._checkpoint_date = datetime.date.today() - datetime.timedelta(days=2)
self._checkpoint_date = datetime.date.today() - datetime.timedelta(
days=2
)
self._runtime = f"r-{str(self._checkpoint_date)}"
return True return True
@lru_cache @lru_cache

Wyświetl plik

@ -1,9 +1,14 @@
from datetime import date
from os.path import join as pjoin from os.path import join as pjoin
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
import pytest import pytest
from repo2docker.buildpacks import LegacyBinderDockerBuildPack, PythonBuildPack from repo2docker.buildpacks import (
BaseImage,
LegacyBinderDockerBuildPack,
PythonBuildPack,
)
from repo2docker.utils import chdir from repo2docker.utils import chdir
@ -46,3 +51,25 @@ def test_unsupported_python(tmpdir, python_version, base_image):
assert bp.python_version == python_version assert bp.python_version == python_version
with pytest.raises(ValueError): with pytest.raises(ValueError):
bp.render() bp.render()
@pytest.mark.parametrize(
"runtime_txt, expected",
[
(None, (None, None, None)),
("", (None, None, None)),
("abc", ("abc", None, None)),
("abc-001", ("abc", "001", None)),
("abc-001-2025-06-22", ("abc", "001", date(2025, 6, 22))),
("a_b/c-0.0.1-2025-06-22", ("a_b/c", "0.0.1", date(2025, 6, 22))),
],
)
def test_runtime_txt(tmpdir, runtime_txt, expected, base_image):
tmpdir.chdir()
if runtime_txt is not None:
with open("runtime.txt", "w") as f:
f.write(runtime_txt)
base = BaseImage(base_image)
assert base.runtime_info == expected