Skip to content
Open
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
1 change: 1 addition & 0 deletions src/macaron/build_spec_generator/common_spec/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class MacaronBuildToolName(str, Enum):
GRADLE = "gradle"
PIP = "pip"
POETRY = "poetry"
UV = "uv"
FLIT = "flit"
HATCH = "hatch"
CONDA = "conda"
Expand Down
2 changes: 2 additions & 0 deletions src/macaron/build_spec_generator/common_spec/pypi_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ def set_default_build_commands(
build_cmd_spec["command"] = "python -m build --wheel -n".split()
case "poetry":
build_cmd_spec["command"] = "poetry build".split()
case "uv":
build_cmd_spec["command"] = "uv build".split()

case "flit":
# We might also want to deal with existence flit.ini, we can do so via
Expand Down
28 changes: 28 additions & 0 deletions src/macaron/config/defaults.ini
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,34 @@ deploy_arg =
[builder.poetry.ci.deploy]
github_actions = pypa/gh-action-pypi-publish

# This is the spec for the uv packaging tool.
[builder.uv]
entry_conf =
build_configs = pyproject.toml
package_lock = uv.lock
builder =
uv
# build-system information.
build_requires =
uv_build
pdm-backend
build_backend =
uv_build
pdm.backend
# These are the Python interpreters that may be used to load modules.
interpreter =
python
python3
interpreter_flag =
-m
build_arg =
build
deploy_arg =
publish

[builder.uv.ci.deploy]
github_actions = pypa/gh-action-pypi-publish

# This is the spec for Flit packaging tool.
[builder.flit]
entry_conf =
Expand Down
4 changes: 3 additions & 1 deletion src/macaron/slsa_analyzer/build_tool/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# Copyright (c) 2022 - 2025, Oracle and/or its affiliates. All rights reserved.
# Copyright (c) 2022 - 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 build_tool package contains the supported build tools for Macaron."""

from macaron.slsa_analyzer.build_tool.conda import Conda
from macaron.slsa_analyzer.build_tool.flit import Flit
from macaron.slsa_analyzer.build_tool.hatch import Hatch
from macaron.slsa_analyzer.build_tool.uv import Uv

from .base_build_tool import BaseBuildTool
from .docker import Docker
Expand All @@ -23,6 +24,7 @@
Gradle(),
Maven(),
Poetry(),
Uv(),
Flit(),
Hatch(),
Conda(),
Expand Down
18 changes: 13 additions & 5 deletions src/macaron/slsa_analyzer/build_tool/pip.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,21 +70,29 @@ def is_detected(self, target: Component) -> list[BuildToolConfig]:
results: list[BuildToolConfig] = (
[]
)

confidence_score = 1.0
for config_name in self.build_configs:
if config_path := file_exists(repo_path, config_name, filters=self.path_filters):
config_path_relative = config_path.relative_to(repo_path)
if os.path.basename(config_path) == "pyproject.toml":
# Check the build-system section. If it doesn't exist, by default setuptools should be used.
if pyproject.get_build_system(config_path) is None:
results.append((str(config_path.relative_to(repo_path)), confidence_score, None, None))
results.append((str(config_path_relative), confidence_score, None, None))
continue
for tool in self.build_requires + self.build_backend:
if pyproject.build_system_contains_tool(tool, config_path):
results.append((str(config_path.relative_to(repo_path)), confidence_score, None, None))
results.append((str(config_path_relative), confidence_score, None, None))
break
if not results:
# If we still have not found an evidence, we add pip as build tool anyway but with a lower confidence score.
results.append((str(config_path_relative), confidence_score / 2, None, None))
# TODO: For other build configuration files, like setup.py, we need to improve the logic.
# For now we assign a lower confidence score if we already have found other config paths.
elif results:
results.append((str(config_path_relative), confidence_score / 2, None, None))
else:
# TODO: For other build configuration files, like setup.py, we need to improve the logic.
results.append((str(config_path.relative_to(repo_path)), confidence_score, None, None))
confidence_score = confidence_score / 2
results.append((str(config_path_relative), confidence_score, None, None))
return results

def get_dep_analyzer(self) -> DependencyAnalyzer:
Expand Down
181 changes: 181 additions & 0 deletions src/macaron/slsa_analyzer/build_tool/uv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# Copyright (c) 2026 - 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 contains the Uv class which inherits BaseBuildTool.

This module is used to work with repositories that use uv for dependency management.
"""

import os

from cyclonedx_py import __version__ as cyclonedx_version

from macaron.config.defaults import defaults
from macaron.config.global_config import global_config
from macaron.database.table_definitions import Component
from macaron.dependency_analyzer.cyclonedx import DependencyAnalyzer
from macaron.dependency_analyzer.cyclonedx_python import CycloneDxPython
from macaron.slsa_analyzer.build_tool import pyproject
from macaron.slsa_analyzer.build_tool.base_build_tool import BaseBuildTool, BuildToolCommand, file_exists
from macaron.slsa_analyzer.build_tool.language import BuildLanguage
from macaron.slsa_analyzer.checks.check_result import Confidence


class Uv(BaseBuildTool):
"""This class contains the information of the uv build tool."""

def __init__(self) -> None:
"""Initialize instance."""
super().__init__(name="uv", language=BuildLanguage.PYTHON, purl_type="pypi")

def load_defaults(self) -> None:
"""Load the default values from defaults.ini."""
super().load_defaults()
if "builder.uv" in defaults:
for item in defaults["builder.uv"]:
if hasattr(self, item):
setattr(self, item, defaults.get_list("builder.uv", item))

if "builder.uv.ci.deploy" in defaults:
for item in defaults["builder.uv.ci.deploy"]:
if item in self.ci_deploy_kws:
self.ci_deploy_kws[item] = defaults.get_list("builder.uv.ci.deploy", item)

def is_detected(self, target: Component) -> list[tuple[str, float, str | None, str | None]]:
"""
Return the list of build tools and their information used in the target repo.

Parameters
----------
target : Component
The target software component.

Returns
-------
list[tuple[str, float, str | None, str | None]]
Tuples of ``(config_path, confidence_score, build_tool_version, parent_pom)``,
where paths are relative to `repo_path` and `parent_pom` may be ``None``.
"""
repo_path, _, _ = self.resolve_component_detection_target(target)
if not repo_path:
return []

package_lock_exists = ""
for file in self.package_lock:
if file_exists(repo_path, file, filters=self.path_filters):
package_lock_exists = file
break

results: list[tuple[str, float, str | None, str | None]] = []
confidence_score = 1.0
file_paths = (file_exists(repo_path, file, filters=self.path_filters) for file in self.build_configs)
for config_path in file_paths:
if config_path and os.path.basename(config_path) == "pyproject.toml":
if package_lock_exists:
results.append((str(config_path.relative_to(repo_path)), confidence_score, None, None))
elif pyproject.contains_build_tool("uv", config_path):
results.append((str(config_path.relative_to(repo_path)), confidence_score, None, None))
else:
for tool in self.build_requires + self.build_backend:
if pyproject.build_system_contains_tool(tool, config_path):
results.append((str(config_path.relative_to(repo_path)), confidence_score, None, None))
break

confidence_score = confidence_score / 2

return results

def get_dep_analyzer(self) -> DependencyAnalyzer:
"""Create a DependencyAnalyzer for the build tool.

Returns
-------
DependencyAnalyzer
The DependencyAnalyzer object.
"""
return CycloneDxPython(
resources_path=global_config.resources_path,
file_name="python_sbom.json",
tool_name="cyclonedx_py",
tool_version=cyclonedx_version,
)

def is_deploy_command(
self, cmd: BuildToolCommand, excluded_configs: list[str] | None = None, provenance_workflow: str | None = None
) -> tuple[bool, Confidence]:
"""
Determine if the command is a deploy command.

Parameters
----------
cmd: BuildToolCommand
The build tool command object.
excluded_configs: list[str] | None
Build tool commands that are called from these configuration files are excluded.
provenance_workflow: str | None
The relative path to the root CI file that is captured in a provenance or None if provenance is not found.

Returns
-------
tuple[bool, Confidence]
Return True along with the inferred confidence level if the command is a deploy tool command.
"""
if cmd["language"] is not self.language:
return False, Confidence.HIGH

build_cmd = cmd["command"]
cmd_program_name = os.path.basename(build_cmd[0])

deploy_tools = self.publisher if self.publisher else self.builder
deploy_args = self.deploy_arg

if cmd_program_name in self.interpreter and len(build_cmd) > 2 and build_cmd[1] in self.interpreter_flag:
build_cmd = build_cmd[2:]

if not self.match_cmd_args(cmd=build_cmd, tools=deploy_tools, args=deploy_args):
return False, Confidence.HIGH

if excluded_configs and os.path.basename(cmd["ci_path"]) in excluded_configs:
return False, Confidence.HIGH

return True, self.infer_confidence_deploy_command(cmd, provenance_workflow)

def is_package_command(
self, cmd: BuildToolCommand, excluded_configs: list[str] | None = None
) -> tuple[bool, Confidence]:
"""
Determine if the command is a packaging command.

Parameters
----------
cmd: BuildToolCommand
The build tool command object.
excluded_configs: list[str] | None
Build tool commands that are called from these configuration files are excluded.

Returns
-------
tuple[bool, Confidence]
Return True along with the inferred confidence level if the command is a build tool command.
"""
if cmd["language"] is not self.language:
return False, Confidence.HIGH

build_cmd = cmd["command"]
cmd_program_name = os.path.basename(build_cmd[0])
if not cmd_program_name:
return False, Confidence.HIGH

builder = self.packager if self.packager else self.builder
build_args = self.build_arg

if cmd_program_name in self.interpreter and len(build_cmd) > 2 and build_cmd[1] in self.interpreter_flag:
build_cmd = build_cmd[2:]

if not self.match_cmd_args(cmd=build_cmd, tools=builder, args=build_args):
return False, Confidence.HIGH

if excluded_configs and os.path.basename(cmd["ci_path"]) in excluded_configs:
return False, Confidence.HIGH

return True, Confidence.HIGH
12 changes: 12 additions & 0 deletions tests/build_spec_generator/common_spec/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ def test_compose_shell_commands(
["pip"],
id="python_pip_supported",
),
pytest.param(
[
BuildToolFacts(
language="python",
build_tool_name="uv",
confidence=1.0,
)
],
"python",
["uv"],
id="python_uv_supported",
),
pytest.param(
[
BuildToolFacts(
Expand Down
45 changes: 45 additions & 0 deletions tests/build_spec_generator/common_spec/test_pypi_spec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copyright (c) 2026 - 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/.

"""Tests for PyPI build spec defaults."""

import pytest

from macaron.build_spec_generator.common_spec.base_spec import BaseBuildSpecDict, SpecBuildCommandDict
from macaron.build_spec_generator.common_spec.pypi_spec import PyPIBuildSpec


@pytest.mark.parametrize(
("build_tool", "expected_command"),
[
("poetry", ["poetry", "build"]),
("flit", ["flit", "build"]),
("uv", ["uv", "build"]),
],
)
def test_set_default_build_commands_for_pypi_tools(build_tool: str, expected_command: list[str]) -> None:
"""Ensure known PyPI build tools map to expected default build commands."""
spec = PyPIBuildSpec(
BaseBuildSpecDict(
{
"ecosystem": "pypi",
"purl": "pkg:pypi/example@1.0.0",
"language": "python",
"build_tools": [build_tool],
"macaron_version": "test",
"artifact_id": "example",
"version": "1.0.0",
"language_version": [],
"build_commands": [],
}
)
)
build_cmd_spec = SpecBuildCommandDict(
build_tool=build_tool,
command=[],
build_config_path="pyproject.toml",
confidence_score=1.0,
)

spec.set_default_build_commands(build_cmd_spec)
assert build_cmd_spec["command"] == expected_command
Loading
Loading