Skip to content

Commit f0d5e0a

Browse files
committed
feat: add support for all build tools
Signed-off-by: behnazh-w <[email protected]>
1 parent 9b05395 commit f0d5e0a

File tree

34 files changed

+357
-144
lines changed

34 files changed

+357
-144
lines changed

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,9 @@ load-plugins = [
224224
"pylint.extensions.set_membership",
225225
"pylint.extensions.typing",
226226
]
227+
# Disable unsubscriptable-object because Pylint has false positives and this check
228+
# overlaps with mypy's checks. Enable the check when the related issue is resolved:
229+
# https://github.com/pylint-dev/pylint/issues/9549
227230
disable = [
228231
"fixme",
229232
"line-too-long", # Replaced by Flake8 Bugbear B950 check.
@@ -242,6 +245,7 @@ disable = [
242245
"too-many-return-statements",
243246
"too-many-statements",
244247
"duplicate-code",
248+
"unsubscriptable-object",
245249
]
246250

247251
[tool.pylint.MISCELLANEOUS]

src/macaron/build_spec_generator/common_spec/core.py

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
lookup_latest_component,
2424
)
2525
from macaron.errors import GenerateBuildSpecError, QueryMacaronDatabaseError
26+
from macaron.json_tools import json_extract
2627
from macaron.slsa_analyzer.checks.build_tool_check import BuildToolFacts
2728

2829
logger: logging.Logger = logging.getLogger(__name__)
@@ -122,7 +123,7 @@ def compose_shell_commands(cmds_sequence: list[list[str]]) -> str:
122123

123124
def get_macaron_build_tools(
124125
build_tool_facts: Sequence[BuildToolFacts], target_language: str
125-
) -> dict[str, dict[str, str | None]] | None:
126+
) -> dict[str, dict[str, float | str | None]] | None:
126127
"""
127128
Retrieve the Macaron build tool names for supported projects from the database facts.
128129
@@ -142,21 +143,26 @@ def get_macaron_build_tools(
142143
The corresponding Macaron build tool name, config_path, confidence score, optional build tool version,
143144
and optional root config path if present.
144145
"""
145-
build_tools = {}
146+
build_tools: dict[str, dict[str, float | str | None]] = {}
146147
for fact in build_tool_facts:
147148
if fact.language.lower() == target_language:
148149
try:
149150
tool_name = MacaronBuildToolName(fact.build_tool_name).value
150-
build_tool_info = {
151+
current_confidence: float = float(fact.confidence)
152+
build_tool_info: dict[str, float | str | None] = {
151153
"build_config_path": fact.build_config_path,
152-
"confidence_score": fact.confidence,
154+
"confidence_score": current_confidence,
153155
"build_tool_version": fact.build_tool_version,
154156
"root_build_config_path": fact.root_build_config_path,
155157
}
156158
existing_build_tool_info = build_tools.get(tool_name)
159+
existing_confidence = (
160+
existing_build_tool_info.get("confidence_score") if existing_build_tool_info is not None else None
161+
)
157162
if (
158163
existing_build_tool_info is None
159-
or build_tool_info["confidence_score"] > existing_build_tool_info["confidence_score"]
164+
or not isinstance(existing_confidence, float)
165+
or current_confidence > existing_confidence
160166
):
161167
build_tools[tool_name] = build_tool_info
162168
except ValueError:
@@ -166,7 +172,7 @@ def get_macaron_build_tools(
166172

167173
def get_build_tools(
168174
component_id: int, session: sqlalchemy.orm.Session, target_language: str
169-
) -> dict[str, dict[str, float, str | None]] | None:
175+
) -> dict[str, dict[str, float | str | None]] | None:
170176
"""Retrieve the Macaron build tool names for a given component.
171177
172178
Queries the database for build tool facts associated with the specified component ID.
@@ -295,6 +301,36 @@ def get_language_version(
295301
return None
296302

297303

304+
def _build_spec_build_command(
305+
build_tools: dict[str, dict[str, float | str | None]],
306+
build_tool_name: str,
307+
command: list[str],
308+
) -> SpecBuildCommandDict | None:
309+
"""Build a single SpecBuildCommandDict entry for a given build tool."""
310+
build_config_path = json_extract(build_tools, [build_tool_name, "build_config_path"], str)
311+
# build_config_path is a required field.
312+
if build_config_path is None:
313+
return None
314+
315+
root_build_config_path = json_extract(build_tools, [build_tool_name, "root_build_config_path"], str)
316+
build_tool_version = json_extract(build_tools, [build_tool_name, "build_tool_version"], str)
317+
confidence_score = json_extract(build_tools, [build_tool_name, "confidence_score"], float)
318+
if confidence_score is None:
319+
return None
320+
321+
build_spec = SpecBuildCommandDict(
322+
build_tool=build_tool_name,
323+
command=command,
324+
build_config_path=build_config_path,
325+
confidence_score=confidence_score,
326+
)
327+
if root_build_config_path is not None:
328+
build_spec["root_build_config_path"] = root_build_config_path
329+
if build_tool_version is not None:
330+
build_spec["build_tool_version"] = build_tool_version
331+
return build_spec
332+
333+
298334
def gen_generic_build_spec(
299335
purl: PackageURL,
300336
session: sqlalchemy.orm.Session,
@@ -384,30 +420,24 @@ def gen_generic_build_spec(
384420
db_build_command_info or "Cannot find any.",
385421
)
386422
lang_version = get_language_version(db_build_command_info) if db_build_command_info else ""
387-
spec_build_commad_info_list.append(
388-
SpecBuildCommandDict(
389-
build_tool=db_build_command_info.build_tool_name,
390-
command=db_build_command_info.command,
391-
build_config_path=build_tools[db_build_command_info.build_tool_name]["build_config_path"],
392-
root_build_config_path=build_tools[db_build_command_info.build_tool_name]["root_build_config_path"],
393-
build_config_version=build_tools[db_build_command_info.build_tool_name]["build_tool_version"],
394-
confidence_score=build_tools[db_build_command_info.build_tool_name]["confidence_score"],
395-
)
423+
build_spec_command = _build_spec_build_command(
424+
build_tools=build_tools,
425+
build_tool_name=db_build_command_info.build_tool_name,
426+
command=db_build_command_info.command,
396427
)
428+
if build_spec_command is not None:
429+
spec_build_commad_info_list.append(build_spec_command)
397430

398431
# If no build commands were found from the analyze phase, add default commands for the identified build tools.
399432
if not db_build_command_info_list:
400433
for build_tool_name in build_tool_names:
401-
spec_build_commad_info_list.append(
402-
SpecBuildCommandDict(
403-
build_tool=build_tool_name,
404-
command=[],
405-
build_config_path=build_tools[build_tool_name]["build_config_path"],
406-
root_build_config_path=build_tools[build_tool_name]["root_build_config_path"],
407-
build_config_version=build_tools[build_tool_name]["build_tool_version"],
408-
confidence_score=build_tools[build_tool_name]["confidence_score"],
409-
)
434+
build_spec_command = _build_spec_build_command(
435+
build_tools=build_tools,
436+
build_tool_name=build_tool_name,
437+
command=[],
410438
)
439+
if build_spec_command is not None:
440+
spec_build_commad_info_list.append(build_spec_command)
411441

412442
base_build_spec_dict = BaseBuildSpecDict(
413443
{

src/macaron/build_spec_generator/common_spec/maven_spec.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def resolve_fields(self, purl: PackageURL) -> None:
9696

9797
# Resolve and patch build commands.
9898
for build_cmd_spec in self.data["build_commands"]:
99-
if build_cmd_spec["command"] == None:
99+
if not build_cmd_spec["command"]:
100100
self.set_default_build_commands(build_cmd_spec)
101101

102102
for build_command_info in self.data["build_commands"]:

src/macaron/build_spec_generator/common_spec/pypi_spec.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def set_default_build_commands(
6262
# "python -m flit.tomlify"
6363
build_cmd_spec["command"] = "flit build".split()
6464
case "hatch":
65-
build_cmd_spec["command"] = command = "hatch build".split()
65+
build_cmd_spec["command"] = "hatch build".split()
6666
case _:
6767
logger.debug(
6868
"There is no default build command available for the build tools %s.",
@@ -92,7 +92,6 @@ def resolve_fields(self, purl: PackageURL) -> None:
9292

9393
upstream_artifacts: dict[str, list[str]] = {}
9494
pypi_package_json = pypi_registry.find_or_create_pypi_asset(purl.name, purl.version, registry_info)
95-
patched_build_commands: list[SpecBuildCommandDict] = []
9695
build_backends_set: set[str] = set()
9796
parsed_build_requires: dict[str, str] = {}
9897
sdist_build_requires: dict[str, str] = {}
@@ -257,6 +256,8 @@ def resolve_fields(self, purl: PackageURL) -> None:
257256
if not self.data["has_binaries"]:
258257
for build_cmd_spec in self.data["build_commands"]:
259258
self.set_default_build_commands(build_cmd_spec)
259+
else:
260+
self.data["build_commands"] = []
260261
self.data["upstream_artifacts"] = upstream_artifacts
261262

262263
def add_parsed_requirement(self, build_requirements: dict[str, str], requirement: str) -> None:

src/macaron/build_spec_generator/reproducible_central/reproducible_central.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,13 @@ def gen_reproducible_central_build_spec(build_spec: BaseBuildSpecDict) -> str |
8888
# Add -Dmaven.test.skip for Maven builds.
8989
# TODO: Use the build tool associated with the build command once
9090
# https://github.com/oracle/macaron/issues/1300 is closed.
91-
adapted_build_commands = [
92-
cmd[:1] + ["-Dmaven.test.skip=true"] + cmd[1:] if ReproducibleCentralBuildTool.MAVEN in cmd[0] else cmd
93-
for cmd in build_spec["build_commands"]
94-
]
91+
adapted_build_commands: list[list[str]] = []
92+
for build_command in build_spec["build_commands"]:
93+
command = build_command["command"]
94+
if command and ReproducibleCentralBuildTool.MAVEN.value in command[0]:
95+
adapted_build_commands.append(command[:1] + ["-Dmaven.test.skip=true"] + command[1:])
96+
else:
97+
adapted_build_commands.append(command)
9598

9699
template_format_values: dict[str, str] = {
97100
"macaron_version": importlib_metadata.version("macaron"),
@@ -104,9 +107,7 @@ def gen_reproducible_central_build_spec(build_spec: BaseBuildSpecDict) -> str |
104107
"newline": build_spec["newline"],
105108
"buildinfo": f"target/{build_spec['artifact_id']}-{build_spec['version']}.buildinfo",
106109
"jdk": build_spec["language_version"][0],
107-
"command": compose_shell_commands(
108-
[b_info["command"] for b_info in adapted_build_commands["build_commands"] if b_info["command"]]
109-
),
110+
"command": compose_shell_commands([command for command in adapted_build_commands if command]),
110111
}
111112

112113
return STRING_TEMPLATE.format_map(template_format_values)

src/macaron/parsers/pomparser.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def extract_gav_from_pom(pom_file: Path) -> tuple[str | None, str | None, str |
7979
logger.debug("Could not parse pom.xml: %s", str(pom_file))
8080
return None, None, None
8181

82-
def _find_child_text(parent, local_name: str) -> str | None:
82+
def _find_child_text(parent: Element, local_name: str) -> str | None:
8383
# The closing curly brace represents the end of the XML namespace.
8484
elem = next((ch for ch in parent if ch.tag.endswith("}" + local_name)), None)
8585
if elem is None or not elem.text:
@@ -146,7 +146,7 @@ def detect_parent_pom(pom_path: Path, repo_root: str | Path) -> str | None:
146146
if pom_root is None:
147147
return None
148148

149-
def _find_child(elem, local_name: str):
149+
def _find_child(elem: Element, local_name: str) -> Element | None:
150150
return next((ch for ch in elem if ch.tag.endswith("}" + local_name)), None)
151151

152152
parent_elem = _find_child(pom_root, "parent")
@@ -200,18 +200,14 @@ def pom_has_modules(pom_path: Path) -> bool:
200200
if pom_root is None:
201201
return False
202202

203-
def _find_child(elem, local_name: str):
203+
def _find_child(elem: Element, local_name: str) -> Element | None:
204204
return next((ch for ch in elem if ch.tag.endswith("}" + local_name)), None)
205205

206206
modules_elem = _find_child(pom_root, "modules")
207207
if modules_elem is None:
208208
return False
209209

210-
for ch in modules_elem:
211-
if ch.tag.endswith("}module") and ch.text and ch.text.strip():
212-
return True
213-
214-
return False
210+
return any(ch.tag.endswith("}module") and ch.text and ch.text.strip() for ch in modules_elem)
215211

216212

217213
def find_nearest_modules_pom(

src/macaron/slsa_analyzer/analyzer.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,8 +1050,6 @@ def _determine_build_tools(self, analyze_ctx: AnalyzeContext, git_service: BaseG
10501050
continue
10511051

10521052
if build_tool.match_purl_type(analyze_ctx.component.type):
1053-
if build_tool.name not in ["pip", "maven", "hatch", "gradle"]:
1054-
continue
10551053
logger.info(
10561054
"Checking if the repo %s uses build tool %s",
10571055
analyze_ctx.component.repository.complete_name,

src/macaron/slsa_analyzer/build_tool/base_build_tool.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,7 @@ def file_exists(
9595
path: str,
9696
file_name: str,
9797
filters: list[str] | None = None,
98-
predicate: Callable[[Path, Any], bool] | None = None,
99-
*predicate_args: Any,
98+
predicate: Callable[..., bool] | None = None,
10099
**predicate_kwargs: Any,
101100
) -> Path | None:
102101
"""Search recursively for the first matching file, optionally validating it with a predicate.
@@ -119,9 +118,7 @@ def file_exists(
119118
Optional callable used to validate a matched file. If provided, a file is
120119
accepted only if ``predicate(candidate_path, *predicate_args, **predicate_kwargs)``
121120
returns ``True``.
122-
*predicate_args : Any
123-
Positional arguments forwarded to `predicate`.
124-
**predicate_kwargs : Any
121+
predicate_kwargs : Any
125122
Keyword arguments forwarded to `predicate`.
126123
127124
Returns
@@ -136,7 +133,7 @@ def file_exists(
136133
root_dir = Path(path)
137134

138135
def _accepted(p: Path) -> bool:
139-
return True if predicate is None else bool(predicate(p, *predicate_args, **predicate_kwargs))
136+
return True if predicate is None else bool(predicate(p, **predicate_kwargs))
140137

141138
# Check for file directly at root.
142139
if target_path := find_first_matching_file(root_dir, file_name):

src/macaron/slsa_analyzer/build_tool/conda.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2025 - 2025, Oracle and/or its affiliates. All rights reserved.
1+
# Copyright (c) 2025 - 2026, Oracle and/or its affiliates. All rights reserved.
22
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/.
33

44
"""This module contains the Conda class which inherits BaseBuildTool.
@@ -66,7 +66,13 @@ def is_detected(
6666
Tuples of ``(config_path, confidence_score, build_tool_version, parent_pom)``,
6767
where paths are relative to `repo_path` and `parent_pom` may be ``None``.
6868
"""
69-
return any(file_exists(repo_path, file, filters=self.path_filters) for file in self.build_configs)
69+
results: list[tuple[str, float, str | None, str | None]] = []
70+
confidence_score = 1.0
71+
for config_name in self.build_configs:
72+
if config_path := file_exists(repo_path, config_name, filters=self.path_filters):
73+
results.append((str(config_path.relative_to(repo_path)), confidence_score, None, None))
74+
confidence_score = confidence_score / 2
75+
return results
7076

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

src/macaron/slsa_analyzer/build_tool/docker.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2023 - 2025, Oracle and/or its affiliates. All rights reserved.
1+
# Copyright (c) 2023 - 2026, Oracle and/or its affiliates. All rights reserved.
22
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/.
33

44
"""This module contains the Docker class which inherits BaseBuildTool.
@@ -55,4 +55,10 @@ def is_detected(
5555
Tuples of ``(config_path, confidence_score, build_tool_version, parent_pom)``,
5656
where paths are relative to `repo_path` and `parent_pom` may be ``None``.
5757
"""
58-
return any(file_exists(repo_path, file, filters=self.path_filters) for file in self.build_configs)
58+
results: list[tuple[str, float, str | None, str | None]] = []
59+
confidence_score = 1.0
60+
for config_name in self.build_configs:
61+
if config_path := file_exists(repo_path, config_name, filters=self.path_filters):
62+
results.append((str(config_path.relative_to(repo_path)), confidence_score, None, None))
63+
confidence_score = confidence_score / 2
64+
return results

0 commit comments

Comments
 (0)