From 7417b5c9967a642fc8c5da69d8f2782e47d53ec3 Mon Sep 17 00:00:00 2001 From: Abhinav Pradeep Date: Mon, 15 Dec 2025 10:36:56 +1000 Subject: [PATCH 1/5] fix: address bugs in gen-build-spec Signed-off-by: Abhinav Pradeep --- .../build_spec_generator.py | 4 +- .../build_spec_generator/common_spec/core.py | 2 +- .../common_spec/pypi_spec.py | 36 ++++++++++------ .../dockerfile/pypi_dockerfile_output.py | 43 +++++++++++++++++-- 4 files changed, 64 insertions(+), 21 deletions(-) diff --git a/src/macaron/build_spec_generator/build_spec_generator.py b/src/macaron/build_spec_generator/build_spec_generator.py index 9d7fd94ca..e66be4ac2 100644 --- a/src/macaron/build_spec_generator/build_spec_generator.py +++ b/src/macaron/build_spec_generator/build_spec_generator.py @@ -98,8 +98,8 @@ def gen_build_spec_for_purl( case BuildSpecFormat.DOCKERFILE: try: build_spec_content = gen_dockerfile(build_spec) - except ValueError as error: - logger.error("Error while serializing the build spec: %s.", error) + except GenerateBuildSpecError as error: + logger.error("Error while generating the build spec: %s.", error) return os.EX_DATAERR build_spec_file_path = os.path.join(build_spec_dir_path, "dockerfile.buildspec") diff --git a/src/macaron/build_spec_generator/common_spec/core.py b/src/macaron/build_spec_generator/common_spec/core.py index 26b2f329f..4c2cf1ecd 100644 --- a/src/macaron/build_spec_generator/common_spec/core.py +++ b/src/macaron/build_spec_generator/common_spec/core.py @@ -378,7 +378,7 @@ def gen_generic_build_spec( "purl": str(purl), "language": target_language, "build_tools": build_tool_names, - "build_commands": [selected_build_command], + "build_commands": [selected_build_command] if selected_build_command else [], } ) ECOSYSTEMS[purl.type.upper()].value(base_build_spec_dict).resolve_fields(purl) diff --git a/src/macaron/build_spec_generator/common_spec/pypi_spec.py b/src/macaron/build_spec_generator/common_spec/pypi_spec.py index 999afbb19..d9bfd4b82 100644 --- a/src/macaron/build_spec_generator/common_spec/pypi_spec.py +++ b/src/macaron/build_spec_generator/common_spec/pypi_spec.py @@ -155,6 +155,16 @@ def resolve_fields(self, purl: PackageURL) -> None: chronologically_likeliest_version = ( pypi_package_json.get_chronologically_suitable_setuptools_version() ) + try: + # Get information from the wheel file name. + logger.debug(pypi_package_json.wheel_filename) + _, _, _, tags = parse_wheel_filename(pypi_package_json.wheel_filename) + for tag in tags: + wheel_name_python_version_list.append(tag.interpreter) + wheel_name_platforms.add(tag.platform) + logger.debug(python_version_set) + except InvalidWheelFilename: + logger.debug("Could not parse wheel file name to extract version") except SourceCodeError: logger.debug("Could not find pure wheel matching this PURL") @@ -214,17 +224,6 @@ def resolve_fields(self, purl: PackageURL) -> None: except (InvalidRequirement, InvalidSpecifier) as error: logger.debug("Malformed requirement encountered %s : %s", requirement, error) - try: - # Get information from the wheel file name. - logger.debug(pypi_package_json.wheel_filename) - _, _, _, tags = parse_wheel_filename(pypi_package_json.wheel_filename) - for tag in tags: - wheel_name_python_version_list.append(tag.interpreter) - wheel_name_platforms.add(tag.platform) - logger.debug(python_version_set) - except InvalidWheelFilename: - logger.debug("Could not parse wheel file name to extract version") - self.data["language_version"] = list(python_version_set) or wheel_name_python_version_list # Use the default build command for pure Python packages. @@ -243,9 +242,18 @@ def resolve_fields(self, purl: PackageURL) -> None: if not patched_build_commands: # Resolve and patch build commands. - selected_build_commands = self.data["build_commands"] or self.get_default_build_commands( - self.data["build_tools"] - ) + + # To ensure that selected_build_commands is never empty, we seed with the fallback + # command of python -m build --wheel -n + if self.data["build_commands"]: + selected_build_commands = self.data["build_commands"] + else: + self.data["build_commands"] = ["python -m build --wheel -n".split()] + selected_build_commands = ( + self.get_default_build_commands(self.data["build_tools"]) or self.data["build_commands"] + ) + + logger.debug(selected_build_commands) patched_build_commands = ( patch_commands( diff --git a/src/macaron/build_spec_generator/dockerfile/pypi_dockerfile_output.py b/src/macaron/build_spec_generator/dockerfile/pypi_dockerfile_output.py index ef2360a5c..e89fb91dd 100644 --- a/src/macaron/build_spec_generator/dockerfile/pypi_dockerfile_output.py +++ b/src/macaron/build_spec_generator/dockerfile/pypi_dockerfile_output.py @@ -4,6 +4,7 @@ """This module implements the logic to generate a dockerfile from a Python buildspec.""" import logging +import re from textwrap import dedent from packaging.specifiers import InvalidSpecifier, SpecifierSet @@ -35,8 +36,7 @@ def gen_dockerfile(buildspec: BaseBuildSpecDict) -> str: """ language_version: str | None = pick_specific_version(buildspec) if language_version is None: - logger.debug("Could not derive a specific interpreter version.") - raise GenerateBuildSpecError("Could not derive specific interpreter version.") + raise GenerateBuildSpecError("Could not derive specific interpreter version") backend_install_commands: str = " && ".join(build_backend_commands(buildspec)) build_tool_install: str = "" if ( @@ -124,8 +124,18 @@ def pick_specific_version(buildspec: BaseBuildSpecDict) -> str | None: try: version_set &= SpecifierSet(version) except InvalidSpecifier as error: - logger.debug("Malformed interpreter version encountered: %s (%s)", version, error) - return None + logger.debug("Non-standard interpreter version encountered: %s (%s)", version, error) + # Whilst the Python tags specify interpreter implementation + # as well as version, with no standard way to parse out the + # implementation, we can attempt to heuristically: + try_parse_version = infer_interpreter_version(version) + if try_parse_version: + try: + version_set &= SpecifierSet(f">={try_parse_version}") + except InvalidSpecifier as error_for_retry: + logger.debug("Could not parse interpreter version from: %s (%s)", version, error_for_retry) + + logger.debug(version_set) # Now to get the latest acceptable one, we can step through all interpreter # versions. For the most accurate result, we can query python.org for a @@ -141,6 +151,31 @@ def pick_specific_version(buildspec: BaseBuildSpecDict) -> str | None: return None +def infer_interpreter_version(tag: str) -> str | None: + """Infer interpreter version from Python-tag. + + Parameters + ---------- + tag: Python-tag, likely inferred from wheel name. + + + Returns + ------- + str: interpreter version inferred from Python-tag + """ + # We will parse the interpreter version of CPython or just + # whatever generic Python version is specified. + pattern = re.compile(r"^(py|cp)(\d{1,3})$") + parsed_tag = pattern.match(tag) + if parsed_tag: + digits = parsed_tag.group(2) + # As match succeeded len(digits) \in {1,2,3} + if len(digits) == 1: + return parsed_tag.group(2) + return f"{digits[0]}.{digits[1:]}" + return None + + def build_backend_commands(buildspec: BaseBuildSpecDict) -> list[str]: """Generate the installation commands for each inferred build backend. From 0a856efac25d6b0e03bfede065fd141bc5687b92 Mon Sep 17 00:00:00 2001 From: Abhinav Pradeep Date: Tue, 6 Jan 2026 11:25:20 +1000 Subject: [PATCH 2/5] feat: using the latest patch and upgraded setuptools version. Signed-off-by: Abhinav Pradeep --- .../dockerfile/pypi_dockerfile_output.py | 123 ++++++++++++++++-- 1 file changed, 112 insertions(+), 11 deletions(-) diff --git a/src/macaron/build_spec_generator/dockerfile/pypi_dockerfile_output.py b/src/macaron/build_spec_generator/dockerfile/pypi_dockerfile_output.py index e89fb91dd..0f633933d 100644 --- a/src/macaron/build_spec_generator/dockerfile/pypi_dockerfile_output.py +++ b/src/macaron/build_spec_generator/dockerfile/pypi_dockerfile_output.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025 - 2025, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2025 - 2026, Oracle and/or its affiliates. All rights reserved. # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. """This module implements the logic to generate a dockerfile from a Python buildspec.""" @@ -7,11 +7,13 @@ import re from textwrap import dedent +from bs4 import BeautifulSoup from packaging.specifiers import InvalidSpecifier, SpecifierSet from packaging.version import InvalidVersion, Version from macaron.build_spec_generator.common_spec.base_spec import BaseBuildSpecDict from macaron.errors import GenerateBuildSpecError +from macaron.util import send_get_http_raw logger: logging.Logger = logging.getLogger(__name__) @@ -37,6 +39,11 @@ def gen_dockerfile(buildspec: BaseBuildSpecDict) -> str: language_version: str | None = pick_specific_version(buildspec) if language_version is None: raise GenerateBuildSpecError("Could not derive specific interpreter version") + try: + version = Version(language_version) + except InvalidVersion as error: + logger.debug("Ran into issue converting %s to a version: %s", language_version, error) + raise GenerateBuildSpecError("Derived interpreter version could not be parsed") from error backend_install_commands: str = " && ".join(build_backend_commands(buildspec)) build_tool_install: str = "" if ( @@ -49,6 +56,7 @@ def gen_dockerfile(buildspec: BaseBuildSpecDict) -> str: build_tool_install = ( f"pip install {buildspec['build_tools'][0]} && if test -f \"flit.ini\"; then python -m flit.tomlify; fi && " ) + modern_build_command = build_tool_install + " ".join(x for x in buildspec["build_commands"][0]) dockerfile_content = f""" #syntax=docker/dockerfile:1.10 FROM oraclelinux:9 @@ -71,13 +79,22 @@ def gen_dockerfile(buildspec: BaseBuildSpecDict) -> str: gcc-c++ gdb lzma glibc-devel libstdc++-devel openssl-devel \\ readline-devel zlib-devel libzstd-devel libffi-devel bzip2-devel \\ xz-devel sqlite sqlite-devel sqlite-libs libuuid-devel gdbm-libs \\ - perf expat expat-devel mpdecimal python3-pip + perf expat expat-devel mpdecimal python3-pip \\ + perl perl-File-Compare + + {openssl_install_commands(version)} + + ENV LD_LIBRARY_PATH=/opt/openssl/lib + ENV CPPFLAGS=-I/opt/openssl/include + ENV LDFLAGS=-L/opt/openssl/lib # Build interpreter and create venv RUN < str: EOF # Run the build - RUN {"source /deps/bin/activate && " + build_tool_install + " ".join(x for x in buildspec["build_commands"][0])} + RUN source /deps/bin/activate && {modern_build_command if version in SpecifierSet(">=3.6") else "python setup.py bdist_wheel"} """ return dedent(dockerfile_content) +def openssl_install_commands(version: Version) -> str: + """Appropriate openssl install commands for a given CPython version. + + Parameters + ---------- + version: Version + CPython version we are trying to build + + Returns + ------- + str + Install commands for the corresponding openssl version + """ + # As per https://peps.python.org/pep-0644, all Python >= 3.10 requires at least OpenSSL 1.1.1. + if version in SpecifierSet(">=3.10"): + openssl_version = "1.1.1w" + source_url = "https://www.openssl.org/source/old/1.1.1/openssl-1.1.1w.tar.gz" + # From the same document, "Python versions 3.6 to 3.9 are compatible with OpenSSL 1.0.2, + # 1.1.0, and 1.1.1". As an attempt to generalize for any >= 3.3, we use OpenSSL 1.0.2. + else: + openssl_version = "1.0.2u" + source_url = "https://www.openssl.org/source/old/1.0.2/openssl-1.0.2u.tar.gz" + + return f"""# Build OpenSSL {openssl_version} + RUN < str | None: """Find the latest python interpreter version satisfying inferred constraints. @@ -118,8 +169,9 @@ def pick_specific_version(buildspec: BaseBuildSpecDict) -> str | None: String in format major.minor.patch for the latest valid Python interpreter version, or None if no such version can be found. """ - # We can most smoothly rebuild Python 3.0.0 and above on OL - version_set = SpecifierSet(">=3.0.0") + # We cannot create virtual environments for Python versions <= 3.3.0, as + # it did not exist back then + version_set = SpecifierSet(">=3.4.0") for version in buildspec["language_version"]: try: version_set &= SpecifierSet(version) @@ -137,14 +189,14 @@ def pick_specific_version(buildspec: BaseBuildSpecDict) -> str | None: logger.debug(version_set) - # Now to get the latest acceptable one, we can step through all interpreter + # Now to get the earliest acceptable one, we can step through all interpreter # versions. For the most accurate result, we can query python.org for a - # list of all versions, but for now we can approximate by stepping down - # through every minor version from 3.14.0 to 3.0.0 - for minor in range(14, -1, -1): + # list of all versions, but for now we can approximate by stepping up + # through every minor version from 3.3.0 to 3.14.0 + for minor in range(3, 15, 1): try: if Version(f"3.{minor}.0") in version_set: - return f"3.{minor}.0" + return get_latest_patch(3, minor) except InvalidVersion as error: logger.debug("Ran into issue converting %s to a version: %s", minor, error) return None @@ -176,6 +228,52 @@ def infer_interpreter_version(tag: str) -> str | None: return None +def get_latest_patch(major: int, minor: int) -> str: + """Given major and minor interpreter version, return latest CPython patched version. + + Parameters + ---------- + major: int + Major component of version + minor: int + Minor component of version + + Returns + ------- + str + Full major.minor.patch version string corresponding to latest + patch for input major and minor. + """ + # We install CPython source + response = send_get_http_raw("https://www.python.org/ftp/python/") + if not response: + raise GenerateBuildSpecError("Failed to fetch index of CPython versions.") + html = response.content.decode("utf-8") + soup = BeautifulSoup(html, "html.parser") + latest_patch: Version | None = None + + # Versions can most reliably be found in anchor tags like: + # {Version}/ + for anchor in soup.find_all("a", href=True): + # Get text enclosed in the anchor tag stripping spaces. + text = anchor.get_text(strip=True) + sanitized_text = text.rstrip("/") + # Try to convert to a version. + try: + parsed_version = Version(sanitized_text) + if parsed_version.major == major and parsed_version.minor == minor: + if latest_patch is None or parsed_version > latest_patch: + latest_patch = parsed_version + except InvalidVersion: + # Try the next tag + continue + + if not latest_patch: + raise GenerateBuildSpecError(f"Failed to infer latest patch for CPython {major}.{minor}") + + return str(latest_patch) + + def build_backend_commands(buildspec: BaseBuildSpecDict) -> list[str]: """Generate the installation commands for each inferred build backend. @@ -193,7 +291,10 @@ def build_backend_commands(buildspec: BaseBuildSpecDict) -> list[str]: return [] commands: list[str] = [] for backend, version_constraint in buildspec["build_requires"].items(): - commands.append(f'/deps/bin/pip install "{backend}{version_constraint}"') + if backend == "setuptools": + commands.append("/deps/bin/pip install --upgrade setuptools") + else: + commands.append(f'/deps/bin/pip install "{backend}{version_constraint}"') # For a stable order on the install commands commands.sort() return commands From 1e41d00d2bbe114d6beb638a618c1dda8e8ac840 Mon Sep 17 00:00:00 2001 From: Abhinav Pradeep Date: Mon, 12 Jan 2026 13:43:44 +1000 Subject: [PATCH 3/5] feat: addressing pitfalls and introducing dependancy based Python verion inference. Signed-off-by: Abhinav Pradeep --- .../common_spec/pypi_spec.py | 9 +++- .../dockerfile/pypi_dockerfile_output.py | 12 +++-- .../package_registry/pypi_registry.py | 48 ++++++++++++++++++- 3 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/macaron/build_spec_generator/common_spec/pypi_spec.py b/src/macaron/build_spec_generator/common_spec/pypi_spec.py index d9bfd4b82..0ea7657b3 100644 --- a/src/macaron/build_spec_generator/common_spec/pypi_spec.py +++ b/src/macaron/build_spec_generator/common_spec/pypi_spec.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025 - 2025, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2025 - 2026, Oracle and/or its affiliates. All rights reserved. # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. """This module includes build specification and helper classes for PyPI packages.""" @@ -237,6 +237,13 @@ def resolve_fields(self, purl: PackageURL) -> None: build_backends_set.add("setuptools.build_meta") logger.debug("Combined build-requires: %s", parsed_build_requires) + + for package, constraint in parsed_build_requires.items(): + package_requirement = package + constraint + python_version_constraints = registry.get_python_requires_for_package_requirement(package_requirement) + if python_version_constraints: + self.data["language_version"].append(python_version_constraints) + self.data["build_requires"] = parsed_build_requires self.data["build_backends"] = list(build_backends_set) diff --git a/src/macaron/build_spec_generator/dockerfile/pypi_dockerfile_output.py b/src/macaron/build_spec_generator/dockerfile/pypi_dockerfile_output.py index 0f633933d..b03095890 100644 --- a/src/macaron/build_spec_generator/dockerfile/pypi_dockerfile_output.py +++ b/src/macaron/build_spec_generator/dockerfile/pypi_dockerfile_output.py @@ -57,6 +57,11 @@ def gen_dockerfile(buildspec: BaseBuildSpecDict) -> str: f"pip install {buildspec['build_tools'][0]} && if test -f \"flit.ini\"; then python -m flit.tomlify; fi && " ) modern_build_command = build_tool_install + " ".join(x for x in buildspec["build_commands"][0]) + legacy_build_command = ( + 'if test -f "setup.py"; then pip install wheel && python setup.py bdist_wheel; ' + "else python -m build --wheel -n; fi" + ) + dockerfile_content = f""" #syntax=docker/dockerfile:1.10 FROM oraclelinux:9 @@ -115,7 +120,7 @@ def gen_dockerfile(buildspec: BaseBuildSpecDict) -> str: EOF # Run the build - RUN source /deps/bin/activate && {modern_build_command if version in SpecifierSet(">=3.6") else "python setup.py bdist_wheel"} + RUN source /deps/bin/activate && {modern_build_command if version in SpecifierSet(">=3.6") else legacy_build_command} """ return dedent(dockerfile_content) @@ -134,8 +139,9 @@ def openssl_install_commands(version: Version) -> str: str Install commands for the corresponding openssl version """ - # As per https://peps.python.org/pep-0644, all Python >= 3.10 requires at least OpenSSL 1.1.1. - if version in SpecifierSet(">=3.10"): + # As per https://peps.python.org/pep-0644, all Python >= 3.10 requires at least OpenSSL 1.1.1, + # and 3.6 to 3.9 can be compiled with OpenSSL 1.1.1. Therefore, we compile as below: + if version in SpecifierSet(">=3.6"): openssl_version = "1.1.1w" source_url = "https://www.openssl.org/source/old/1.1.1/openssl-1.1.1w.tar.gz" # From the same document, "Python versions 3.6 to 3.9 are compatible with OpenSSL 1.0.2, diff --git a/src/macaron/slsa_analyzer/package_registry/pypi_registry.py b/src/macaron/slsa_analyzer/package_registry/pypi_registry.py index ce8630d37..515b77776 100644 --- a/src/macaron/slsa_analyzer/package_registry/pypi_registry.py +++ b/src/macaron/slsa_analyzer/package_registry/pypi_registry.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023 - 2025, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2023 - 2026, Oracle and/or its affiliates. All rights reserved. # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. """The module provides abstractions for the pypi package registry.""" @@ -22,6 +22,8 @@ import requests from bs4 import BeautifulSoup, Tag +from packaging.requirements import InvalidRequirement, Requirement +from packaging.version import InvalidVersion, Version from macaron.config.defaults import defaults from macaron.errors import ConfigurationError, InvalidHTTPResponseError, SourceCodeError @@ -539,6 +541,50 @@ def get_matching_setuptools_version(self, package_release_datetime: datetime) -> # Return default just in case. return defaults.get("heuristic.pypi", "default_setuptools") + def get_python_requires_for_package_requirement(self, package_requirement: str) -> str | None: + """Return the Python version constraint string for earliest version of the package satisfying package_requirement. + + Parameters + ---------- + package_constraint: str + pip style requirement string. + + Returns + ------- + str | None + Corresponding Python version constraint string. + """ + try: + parsed_requirement = Requirement(package_requirement) + endpoint = urllib.parse.urljoin(self.registry_url, f"pypi/{parsed_requirement.name}/json") + json = self.download_package_json(endpoint) + releases = json_extract(json, ["releases"], dict) + if releases: + # Find smallest requirement satisfying parsed_requirement.name + version_tuples: list[tuple[str, Version]] = [] + for version in releases.keys(): + try: + version_name = str(version) + parsed_version = Version(version_name) + if parsed_version in parsed_requirement.specifier: + version_tuple = (version_name, parsed_version) + version_tuples.append(version_tuple) + except InvalidVersion: + continue + if not version_tuples: + return None + lowest_staisfying_version = min(version_tuples, key=lambda version_tuple: version_tuple[1]) + release_info = releases[lowest_staisfying_version[0]] + if isinstance(release_info, list) and release_info: + release = release_info[0] + if isinstance(release, dict): + constraint_specification = release.get("requires_python") + if isinstance(constraint_specification, str): + return constraint_specification + return None + except InvalidRequirement: + return None + @staticmethod def extract_attestation(attestation_data: dict) -> dict | None: """Extract the first attestation file from a PyPI attestation response. From 8367083d63e40ed8fb96065cbaa4278ea544db13 Mon Sep 17 00:00:00 2001 From: Abhinav Pradeep Date: Tue, 13 Jan 2026 13:34:31 +1000 Subject: [PATCH 4/5] fix: updated integration tests to reflect new feature. Signed-off-by: Abhinav Pradeep --- .../common_spec/pypi_spec.py | 5 +++- .../expected_default.buildspec | 2 +- .../expected_dockerfile.buildspec | 29 +++++++++++++++---- .../expected_default.buildspec | 5 ++-- .../expected_dockerfile.buildspec | 27 +++++++++++++---- .../pypi_toga/expected_default.buildspec | 3 +- .../pypi_toga/expected_dockerfile.buildspec | 29 +++++++++++++++---- 7 files changed, 78 insertions(+), 22 deletions(-) diff --git a/src/macaron/build_spec_generator/common_spec/pypi_spec.py b/src/macaron/build_spec_generator/common_spec/pypi_spec.py index 0ea7657b3..8fc9d1ea8 100644 --- a/src/macaron/build_spec_generator/common_spec/pypi_spec.py +++ b/src/macaron/build_spec_generator/common_spec/pypi_spec.py @@ -120,6 +120,7 @@ def resolve_fields(self, purl: PackageURL) -> None: python_version_set: set[str] = set() wheel_name_python_version_list: list[str] = [] wheel_name_platforms: set[str] = set() + version_constraint_set: set[str] = set() # Precautionary fallback to default version chronologically_likeliest_version: str = defaults.get("heuristic.pypi", "default_setuptools") @@ -242,7 +243,9 @@ def resolve_fields(self, purl: PackageURL) -> None: package_requirement = package + constraint python_version_constraints = registry.get_python_requires_for_package_requirement(package_requirement) if python_version_constraints: - self.data["language_version"].append(python_version_constraints) + version_constraint_set.add(python_version_constraints) + + self.data["language_version"] = sorted(version_constraint_set) self.data["build_requires"] = parsed_build_requires self.data["build_backends"] = list(build_backends_set) diff --git a/tests/integration/cases/pypi_cachetools/expected_default.buildspec b/tests/integration/cases/pypi_cachetools/expected_default.buildspec index 0b5d8acfa..58e1da54e 100644 --- a/tests/integration/cases/pypi_cachetools/expected_default.buildspec +++ b/tests/integration/cases/pypi_cachetools/expected_default.buildspec @@ -1,5 +1,5 @@ { - "macaron_version": "0.18.0", + "macaron_version": "0.20.0", "group_id": null, "artifact_id": "cachetools", "version": "6.2.1", diff --git a/tests/integration/cases/pypi_cachetools/expected_dockerfile.buildspec b/tests/integration/cases/pypi_cachetools/expected_dockerfile.buildspec index 749757f91..254f0b56e 100644 --- a/tests/integration/cases/pypi_cachetools/expected_dockerfile.buildspec +++ b/tests/integration/cases/pypi_cachetools/expected_dockerfile.buildspec @@ -10,8 +10,8 @@ RUN dnf -y install gcc make # Download and unzip interpreter RUN <=3.10" + ">=3.6", + ">=3.8" ], "ecosystem": "pypi", "purl": "pkg:pypi/markdown-it-py@4.0.0", diff --git a/tests/integration/cases/pypi_markdown-it-py/expected_dockerfile.buildspec b/tests/integration/cases/pypi_markdown-it-py/expected_dockerfile.buildspec index 981619196..e4133eb2c 100644 --- a/tests/integration/cases/pypi_markdown-it-py/expected_dockerfile.buildspec +++ b/tests/integration/cases/pypi_markdown-it-py/expected_dockerfile.buildspec @@ -10,8 +10,8 @@ RUN dnf -y install gcc make # Download and unzip interpreter RUN <=3.8", ">=3.9" ], "ecosystem": "pypi", diff --git a/tests/integration/cases/pypi_toga/expected_dockerfile.buildspec b/tests/integration/cases/pypi_toga/expected_dockerfile.buildspec index 47e1e012a..d50340d8b 100644 --- a/tests/integration/cases/pypi_toga/expected_dockerfile.buildspec +++ b/tests/integration/cases/pypi_toga/expected_dockerfile.buildspec @@ -10,8 +10,8 @@ RUN dnf -y install gcc make # Download and unzip interpreter RUN < Date: Tue, 13 Jan 2026 13:42:09 +1000 Subject: [PATCH 5/5] fix: updated unit tests to reflect new feature. Signed-off-by: Abhinav Pradeep --- .../test_pypi_dockerfile_output.ambr | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/tests/build_spec_generator/dockerfile/__snapshots__/test_pypi_dockerfile_output.ambr b/tests/build_spec_generator/dockerfile/__snapshots__/test_pypi_dockerfile_output.ambr index 696ee6f8d..655628572 100644 --- a/tests/build_spec_generator/dockerfile/__snapshots__/test_pypi_dockerfile_output.ambr +++ b/tests/build_spec_generator/dockerfile/__snapshots__/test_pypi_dockerfile_output.ambr @@ -13,8 +13,8 @@ # Download and unzip interpreter RUN <