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 ee67578c9..e7ce40ee9 100644 --- a/src/macaron/build_spec_generator/common_spec/pypi_spec.py +++ b/src/macaron/build_spec_generator/common_spec/pypi_spec.py @@ -132,7 +132,7 @@ def resolve_fields(self, purl: PackageURL) -> None: logger.debug("From package JSON inferred Python constraints: %s", python_version_set) - self.data["has_binaries"] = not pypi_package_json.has_pure_wheel() + self.data["has_binaries"] = pypi_package_json.has_non_pure_wheel() if self.data["has_binaries"]: logger.debug("Can not find a pure wheel") 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 67d1c6308..62954e312 100644 --- a/src/macaron/build_spec_generator/dockerfile/pypi_dockerfile_output.py +++ b/src/macaron/build_spec_generator/dockerfile/pypi_dockerfile_output.py @@ -69,14 +69,14 @@ def gen_dockerfile(buildspec: BaseBuildSpecDict) -> str: "else python -m build --wheel -n; fi" ) - wheel_url: str = "" + # Initialized empty so that the validation script can exit gracefully in the case we find no upstream wheel wheel_name: str = "" - - wheel_urls = buildspec["upstream_artifacts"]["wheels"] - # We currently only look for the pure wheel, if it exists - if wheel_urls: - wheel_url = list(wheel_urls)[0] - wheel_name = wheel_url.rsplit("/", 1)[-1] + wheel_url: str = "" + if "wheels" in buildspec["upstream_artifacts"]: + wheel_urls = buildspec["upstream_artifacts"]["wheels"] + if wheel_urls: + wheel_url = wheel_urls[0] + wheel_name = wheel_url.rsplit("/", 1)[-1] else: logger.debug("We could not find an upstream artifact, and therefore we cannot run validation") diff --git a/src/macaron/slsa_analyzer/package_registry/pypi_registry.py b/src/macaron/slsa_analyzer/package_registry/pypi_registry.py index 579a13d5b..6741fd208 100644 --- a/src/macaron/slsa_analyzer/package_registry/pypi_registry.py +++ b/src/macaron/slsa_analyzer/package_registry/pypi_registry.py @@ -965,13 +965,13 @@ def download_wheel(self) -> bool: logger.debug(error) return False - def has_pure_wheel(self) -> bool: - """Check whether the PURL has a pure wheel from its package json. + def has_non_pure_wheel(self) -> bool: + """Check whether the PURL has any non-pure wheel from its package json. Returns ------- bool - Whether the PURL has a pure wheel or not. + Whether the PURL has any non-pure wheel or not. """ if self.component_version: urls = json_extract(self.package_json, ["releases", self.component_version], list) @@ -982,16 +982,13 @@ def has_pure_wheel(self) -> bool: return False for distribution in urls: file_name: str = distribution.get("filename") or "" - # Parse out and check none and any - # Catch exceptions try: _, _, _, tags = parse_wheel_filename(file_name) - # Check if none and any are in the tags (i.e. the wheel is pure) - if all(tag.abi == "none" and tag.platform == "any" for tag in tags): + # A wheel is non-pure if any tag is not abi=none and platform=any + if any(tag.abi != "none" or tag.platform != "any" for tag in tags): return True except InvalidWheelFilename: logger.debug("Could not parse wheel name.") - return False return False @contextmanager diff --git a/tests/integration/cases/pypi_pytesseract/expected_default.buildspec b/tests/integration/cases/pypi_pytesseract/expected_default.buildspec new file mode 100644 index 000000000..9daecb465 --- /dev/null +++ b/tests/integration/cases/pypi_pytesseract/expected_default.buildspec @@ -0,0 +1,39 @@ +{ + "macaron_version": "0.20.0", + "group_id": null, + "artifact_id": "pytesseract", + "version": "0.3.8", + "git_repo": "https://github.com/madmaze/pytesseract", + "git_tag": "805d3959496232edf2f0feb41af750d5702d85b7", + "newline": "lf", + "language_version": [ + ">=3.7" + ], + "ecosystem": "pypi", + "purl": "pkg:pypi/pytesseract@0.3.8", + "language": "python", + "build_tools": [ + "pip" + ], + "build_commands": [ + [ + "python", + "-m", + "build", + "--wheel", + "-n" + ] + ], + "has_binaries": false, + "build_requires": { + "setuptools": "==67.7.2" + }, + "build_backends": [ + "setuptools.build_meta" + ], + "upstream_artifacts": { + "sdist": [ + "https://files.pythonhosted.org/packages/a3/c9/d6e8903482bd6fb994c32722831d15842dd8b614f94ad9ca735807252671/pytesseract-0.3.8.tar.gz" + ] + } +} diff --git a/tests/integration/cases/pypi_pytesseract/expected_dockerfile.buildspec b/tests/integration/cases/pypi_pytesseract/expected_dockerfile.buildspec new file mode 100644 index 000000000..af9562fd4 --- /dev/null +++ b/tests/integration/cases/pypi_pytesseract/expected_dockerfile.buildspec @@ -0,0 +1,91 @@ + +#syntax=docker/dockerfile:1.10 +FROM oraclelinux:9 + +# Install core tools +RUN dnf -y install which wget tar unzip git + +# Install compiler and make +RUN dnf -y install gcc make + +# Download and unzip interpreter +RUN </validate + [ -n "" ] || { echo "No upstream artifact to validate against."; exit 1; } + # Capture artifacts generated + WHEELS=(/src/dist/*.whl) + # Ensure we only have one artifact + [ ${#WHEELS[@]} -eq 1 ] || { echo "Unexpected artifacts produced!"; exit 1; } + # BUILT_WHEEL is the artifact we built + BUILT_WHEEL=${WHEELS[0]} + # Ensure the artifact produced is not the literal returned by the glob + [ -e $BUILT_WHEEL ] || { echo "No wheels found!"; exit 1; } + # Download the wheel + wget -q + # Compare wheel names + [ $(basename $BUILT_WHEEL) == "" ] || { echo "Wheel name does not match!"; exit 1; } + # Compare file tree + (unzip -Z1 $BUILT_WHEEL | grep -v '\.dist-info' | sort) > built.tree + (unzip -Z1 "" | grep -v '\.dist-info' | sort ) > pypi_artifact.tree + diff -u built.tree pypi_artifact.tree || { echo "File trees do not match!"; exit 1; } + echo "Success!" +EOF + +ENTRYPOINT ["/bin/bash","/validate"] diff --git a/tests/integration/cases/pypi_pytesseract/test.yaml b/tests/integration/cases/pypi_pytesseract/test.yaml new file mode 100644 index 000000000..7407ae002 --- /dev/null +++ b/tests/integration/cases/pypi_pytesseract/test.yaml @@ -0,0 +1,45 @@ +# 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/. + +description: | + Integration test to ensure that has_binaries is not a false positive. + +tags: +- macaron-python-package +- tutorial + +steps: +- name: Run macaron analyze + kind: analyze + options: + command_args: + - -purl + - pkg:pypi/pytesseract@0.3.8 +- name: Generate the buildspec + kind: gen-build-spec + options: + command_args: + - -purl + - pkg:pypi/pytesseract@0.3.8 + - --output-format + - default-buildspec +- name: Compare Buildspec. + kind: compare + options: + kind: default_build_spec + result: output/buildspec/pypi/pytesseract/macaron.buildspec + expected: expected_default.buildspec +- name: Generate the buildspec + kind: gen-build-spec + options: + command_args: + - -purl + - pkg:pypi/pytesseract@0.3.8 + - --output-format + - dockerfile +- name: Compare Dockerfile + kind: compare + options: + kind: dockerfile_build_spec + result: output/buildspec/pypi/pytesseract/dockerfile.buildspec + expected: expected_dockerfile.buildspec